diff --git a/.gitattributes b/.gitattributes index 464971cb..72bff4bf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,9 @@ -* text=auto eol=lf +# Set default behavior to automatically normalize line endings +* text=auto + +# Force CRLF line endings for C# files +*.cs text eol=crlf + +# Force CRLF for other common Windows development files (optional) +*.csproj text eol=crlf +*.sln text eol=crlf diff --git a/.gitignore b/.gitignore index 87860e10..22093f15 100644 --- a/.gitignore +++ b/.gitignore @@ -306,3 +306,6 @@ MultiplayerAssets/ProjectSettings/* !MultiplayerAssets/ProjectSettings/ProjectVersion.txt # Packages !MultiplayerAssets/Packages +/Lobby Servers/Rust Server/target +*.pem +/docs diff --git a/Convert-ToGitHubWiki.ps1 b/Convert-ToGitHubWiki.ps1 new file mode 100644 index 00000000..1dea901a --- /dev/null +++ b/Convert-ToGitHubWiki.ps1 @@ -0,0 +1,348 @@ +param( + [Parameter(Mandatory=$false)] + [string]$InputPath = ".", + + [Parameter(Mandatory=$false)] + [string]$OutputPath = "./wiki-output", + + [Parameter(Mandatory=$false)] + [switch]$Flatten, + + [Parameter(Mandatory=$false)] + [switch]$InPlace +) + +<# +.SYNOPSIS + Converts XMLDoc2Markdown output to GitHub Wiki compatible format. + +.DESCRIPTION + This script processes markdown files generated by XMLDoc2Markdown and converts them + to be compatible with GitHub Wiki by: + - Removing .md extensions from links + - Removing ./ prefixes from links + - Optionally flattening all files into a single markdown file + - Optionally modifying files in place + +.NOTES + Created with assistance from GitHub Copilot + +.PARAMETER InputPath + The path to the directory containing the markdown files. Default is current directory. + +.PARAMETER OutputPath + The path where converted files will be saved. Default is ./wiki-output. + Ignored if -InPlace is used. + +.PARAMETER Flatten + If specified, all markdown files will be combined into a single file. + +.PARAMETER InPlace + If specified, modifies files in place instead of creating copies. + +.EXAMPLE + .\Convert-ToGitHubWiki.ps1 + Converts all markdown files in current directory to wiki-output folder. + +.EXAMPLE + .\Convert-ToGitHubWiki.ps1 -InPlace + Modifies all markdown files in place. + +.EXAMPLE + .\Convert-ToGitHubWiki.ps1 -Flatten -OutputPath "./wiki.md" + Combines all markdown files into a single wiki.md file. +#> + +function Convert-MarkdownLinks { + param( + [string]$Content, + [bool]$UseAnchors = $false + ) + + if ($UseAnchors) { + # Convert relative links to anchor links for flattened output + # Match patterns like [Text](./path/file.md) or [Text](path/file.md) + $Content = [regex]::Replace($Content, '\[([^\]]+)\]\(\.?/?([^)]+?)(?:\.md)?\)', { + param($match) + $linkText = $match.Groups[1].Value + $linkPath = $match.Groups[2].Value + + # Skip external links (http/https) + if ($linkPath -match '^https?://') { + return $match.Value + } + + # Convert path to anchor + # Remove path separators and convert to lowercase + $anchor = $linkPath -replace '[/\\]', '-' -replace '[^a-zA-Z0-9\s-]', '' -replace '\s+', '-' -replace '-+', '-' + $anchor = $anchor.ToLower().Trim('-') + + return "[$linkText](#$anchor)" + }) + } else { + # For GitHub Wiki: Convert links to use only filename (no directory path) + # Match patterns like [Text](./path/to/file.md) or [Text](path/to/file.md#anchor) + $Content = [regex]::Replace($Content, '\[([^\]]+)\]\(\.?/?([^)#]+?)(?:\.md)?(#[^)]+)?\)', { + param($match) + $linkText = $match.Groups[1].Value + $linkPath = $match.Groups[2].Value + $anchor = $match.Groups[3].Value + + # Skip external links (http/https) + if ($linkPath -match '^https?://') { + return $match.Value + } + + # Extract just the filename from the path (remove directory) + $filename = Split-Path -Path $linkPath -Leaf + + # Return with just filename and optional anchor + return "[$linkText]($filename$anchor)" + }) + } + + return $Content +} + +function Get-MarkdownFiles { + param( + [string]$Path, + [string]$ExcludePath = $null, + [switch]$ExcludeIndex + ) + + $files = Get-ChildItem -Path $Path -Filter "*.md" -Recurse | Where-Object { $_.Name -ne "README.md" } + + # Exclude index.md if requested (for flattened output which generates its own TOC) + if ($ExcludeIndex) { + $files = $files | Where-Object { $_.Name -ne "index.md" } + } + + # Exclude files in the output directory + if ($ExcludePath) { + $excludeFullPath = (Resolve-Path -Path $ExcludePath -ErrorAction SilentlyContinue) + if ($excludeFullPath) { + $files = $files | Where-Object { !$_.FullName.StartsWith($excludeFullPath) } + } + } + + return $files +} + +function Flatten-ToSingleFile { + param( + [string]$InputPath, + [string]$OutputFile + ) + + Write-Host "Flattening markdown files into single document..." -ForegroundColor Cyan + + # Get output directory to exclude it from processing + $outputDir = Split-Path -Path $OutputFile -Parent + if ($outputDir) { + $outputDir = Join-Path -Path $InputPath -ChildPath $outputDir + } + + $files = Get-MarkdownFiles -Path $InputPath -ExcludePath $outputDir -ExcludeIndex | Sort-Object FullName + + $combinedContent = @() + + # Build mapping of file paths to their heading anchors (filename -> heading anchor) + $fileToAnchor = @{} + + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Extract first heading + if ($content -match '^#\s+(.+)') { + $heading = $matches[1].Trim() + $anchor = $heading -replace '[^a-zA-Z0-9\s-]', '' -replace '\s+', '-' -replace '-+', '-' + $anchor = $anchor.ToLower().Trim('-') + + # Store mapping: filename (without extension) -> anchor + $filename = [System.IO.Path]::GetFileNameWithoutExtension($file.Name) + $fileToAnchor[$filename.ToLower()] = $anchor + + # Also store full relative path mapping (with forward slashes, no extension) + $pathWithoutExt = $relativePath -replace '\.md$', '' -replace '\\', '/' + $fileToAnchor[$pathWithoutExt.ToLower()] = $anchor + } + } + + # Add table of contents header + $combinedContent += "## Table of Contents" + $combinedContent += "" + + # Build hierarchical TOC based on namespace from files + $tocStructure = @{} + + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Extract first heading and namespace + $heading = $null + $namespace = $null + + if ($content -match '^#\s+(.+)') { + $heading = $matches[1].Trim() + } + + if ($content -match 'Namespace:\s*([^\r\n]+)') { + $namespace = $matches[1].Trim() + } + + if ($heading) { + $anchor = $heading -replace '[^a-zA-Z0-9\s-]', '' -replace '\s+', '-' -replace '-+', '-' + $anchor = $anchor.ToLower().Trim('-') + + # Use namespace if available, otherwise use root + $category = if ($namespace) { $namespace } else { '_root' } + + if (!$tocStructure.ContainsKey($category)) { + $tocStructure[$category] = @() + } + $tocStructure[$category] += @{ Heading = $heading; Anchor = $anchor } + } + } + + # Output TOC with sections + $orderedCategories = $tocStructure.Keys | Sort-Object | Where-Object { $_ -ne '_root' } + + # Root items first if any + if ($tocStructure.ContainsKey('_root')) { + foreach ($item in $tocStructure['_root']) { + $combinedContent += "- [$($item.Heading)](#$($item.Anchor))" + } + $combinedContent += "" + } + + # Then categorized items + foreach ($category in $orderedCategories) { + $combinedContent += "### $category" + $combinedContent += "" + foreach ($item in $tocStructure[$category]) { + $combinedContent += "- [$($item.Heading)](#$($item.Anchor))" + } + $combinedContent += "" + } + + $combinedContent += "---" + $combinedContent += "

" + $combinedContent += "" + + # Add content from each file + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + Write-Host " Processing: $relativePath" + + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + + # Convert links by replacing each known path with its anchor + foreach ($path in $fileToAnchor.Keys) { + $anchor = $fileToAnchor[$path] + $escapedPath = [regex]::Escape($path) + # Replace links like [Text](path) with [Text](#anchor) + $content = $content -replace "\]\($escapedPath\)", "](#$anchor)" + # Also handle links with existing fragment anchors - preserve the fragment + # but this is tricky, so for now just do the simple case + } + + # Add source file comment + $combinedContent += "" + $combinedContent += "" + + # Add content + $combinedContent += $content.TrimEnd() + $combinedContent += "" + $combinedContent += "---" + $combinedContent += "

" + $combinedContent += "" + } + + # Ensure output directory exists + if ($outputDir -and !(Test-Path -Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + + # Write combined file + $combinedContent -join "`n" | Set-Content -Path $OutputFile -Encoding UTF8 -NoNewline + + Write-Host "Created flattened file: $OutputFile" -ForegroundColor Green + Write-Host "Processed $($files.Count) files" -ForegroundColor Green +} + +function Convert-Files { + param( + [string]$InputPath, + [string]$OutputPath, + [bool]$InPlace + ) + + Write-Host "Converting markdown files for GitHub Wiki..." -ForegroundColor Cyan + + $files = Get-MarkdownFiles -Path $InputPath + $count = 0 + + foreach ($file in $files) { + $relativePath = $file.FullName.Substring($InputPath.Length).TrimStart('\', '/') + Write-Host " Processing: $relativePath" + + $content = Get-Content -Path $file.FullName -Raw -Encoding UTF8 + $convertedContent = Convert-MarkdownLinks -Content $content + + if ($InPlace) { + $outputFile = $file.FullName + } else { + $outputFile = Join-Path -Path $OutputPath -ChildPath $relativePath + $outputDir = Split-Path -Path $outputFile -Parent + + if (!(Test-Path -Path $outputDir)) { + New-Item -Path $outputDir -ItemType Directory -Force | Out-Null + } + } + + $convertedContent | Set-Content -Path $outputFile -Encoding UTF8 -NoNewline + $count++ + } + + if ($InPlace) { + Write-Host "Modified $count files in place" -ForegroundColor Green + } else { + Write-Host "Converted $count files to: $OutputPath" -ForegroundColor Green + } +} + +# Main execution +try { + # Resolve full path + $InputPath = Resolve-Path -Path $InputPath -ErrorAction Stop + + if ($Flatten) { + # Determine output file path + if ($OutputPath -like "*.md") { + $flattenedFile = $OutputPath + } else { + $flattenedFile = Join-Path -Path $OutputPath -ChildPath "API-Documentation.md" + } + + Flatten-ToSingleFile -InputPath $InputPath -OutputFile $flattenedFile + } else { + if ($InPlace) { + $confirmation = Read-Host "This will modify files in place. Continue? (y/n)" + if ($confirmation -ne 'y') { + Write-Host "Operation cancelled." -ForegroundColor Yellow + exit + } + } + + Convert-Files -InputPath $InputPath -OutputPath $OutputPath -InPlace $InPlace + } + + Write-Host "`nConversion complete!" -ForegroundColor Green + +} catch { + Write-Host "Error: $_" -ForegroundColor Red + exit 1 +} diff --git a/Directory.Build.targets.EXAMPLE b/Directory.Build.targets.EXAMPLE new file mode 100644 index 00000000..29314c43 --- /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 00000000..c8f09176 --- /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 00000000..ae751d42 --- /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 00000000..c649d397 --- /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 00000000..c6caf42b --- /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 00000000..5bc4c506 --- /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 00000000..52073ea0 --- /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 00000000..68751f16 --- /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 00000000..f323dcbf --- /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 00000000..cd9d574f --- /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 00000000..f80e1d82 --- /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 00000000..2e80b782 --- /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 00000000..db84e870 --- /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 00000000..e863e8b3 --- /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 00000000..bc25a1fb --- /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 00000000..36961fdd --- /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 00000000..286a442a --- /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 00000000..a4c9abc4 --- /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 00000000..f8c9f700 --- /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 00000000..a1335a90 --- /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 00000000..b89c13c3 --- /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.sln b/Multiplayer.sln index cb957169..f8f4eef3 100644 --- a/Multiplayer.sln +++ b/Multiplayer.sln @@ -1,16 +1,36 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{99D6F2A9-1021-4EA5-ACB3-48CBD6FB8D09}") = "Multiplayer", "Multiplayer/Multiplayer.csproj", "{F712C7FB-EEAE-4036-A938-356E022B0455}" +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36121.58 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Multiplayer", "Multiplayer\Multiplayer.csproj", "{F712C7FB-EEAE-4036-A938-356E022B0455}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerAPI", "MultiplayerAPI\MultiplayerAPI.csproj", "{AB0CA646-6E23-42EC-9F24-176CC0331714}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiplayerAPITest", "MultiplayerAPI Tests\MultiplayerAPITest.csproj", "{65E62E81-6FD4-4995-9BF0-3AEF1ED32800}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Release|Any CPU = Release|Any CPU Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.Build.0 = Release|Any CPU {F712C7FB-EEAE-4036-A938-356E022B0455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F712C7FB-EEAE-4036-A938-356E022B0455}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F712C7FB-EEAE-4036-A938-356E022B0455}.Release|Any CPU.Build.0 = Release|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB0CA646-6E23-42EC-9F24-176CC0331714}.Release|Any CPU.Build.0 = Release|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65E62E81-6FD4-4995-9BF0-3AEF1ED32800}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C12E6469-93BC-497D-BEF6-BD6603B6A89C} EndGlobalSection EndGlobal diff --git a/Multiplayer/API/APIProvider.cs b/Multiplayer/API/APIProvider.cs new file mode 100644 index 00000000..da3a8a2c --- /dev/null +++ b/Multiplayer/API/APIProvider.cs @@ -0,0 +1,142 @@ +using DV.Customization.Paint; +using DV.Logic.Job; +using MPAPI.Interfaces; +using MPAPI.Types; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using System; +using System.Collections.Generic; + +namespace Multiplayer.API; + +public class APIProvider : IMultiplayerAPI +{ + internal const string BUILT_AGAINST_API_VERSION = "1.0.0.0"; + + public string SupportedApiVersion => BUILT_AGAINST_API_VERSION; + + public string MultiplayerVersion => Multiplayer.Ver; + + public bool IsMultiplayerLoaded => true; + + public bool IsConnected => NetworkLifecycle.Instance.IsClientRunning || NetworkLifecycle.Instance.IsServerRunning; + + public bool IsHost => NetworkLifecycle.Instance.IsHost(); + + public bool IsDedicatedServer => false; //feature not implemented + + public bool IsSinglePlayer => NetworkLifecycle.Instance.IsServerRunning && (NetworkLifecycle.Instance?.Server.IsSinglePlayer ?? false); + + public event Action OnTick; + public uint TICK_RATE => NetworkLifecycle.TICK_RATE; + public uint CurrentTick => NetworkLifecycle.Instance.Tick; + + public bool TryGetNetId(T obj, out ushort netId) where T : class + { + return NetIdProvider.Instance.TryGetNetId(obj, out netId); + } + + public bool TryGetNetId(T obj, out uint netId) where T : class + { + return NetIdProvider.Instance.TryGetNetId(obj, out netId); + } + + public bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class + { + return NetIdProvider.Instance.TryGetObject(netId, out obj); + } + + public bool TryGetObjectFromNetId(uint netId, out T obj) where T : class + { + return NetIdProvider.Instance.TryGetObject(netId, out obj); + } + + public void SetModCompatibility(string modId, MultiplayerCompatibility compatibility) + { + ModCompatibilityManager.Instance.RegisterCompatibility(modId, compatibility); + } + + public uint RegisterPaintTheme(PaintTheme theme) + { + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) + { + Multiplayer.LogWarning("APIProvider.RegisterPaintTheme() called with null theme or empty AssetName"); + return 0; + } + + if (!NetworkLifecycle.Instance.IsServerRunning || !NetworkLifecycle.Instance.IsClientRunning) + { + Multiplayer.LogWarning("APIProvider.RegisterPaintTheme() called when server or client is not running"); + return 0; + } + + return PaintThemeLookup.Instance.RegisterTheme(theme); + } + + public void UnregisterPaintTheme(PaintTheme theme) + { + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) + { + Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called with null theme or empty AssetName"); + return; + } + + if (!NetworkLifecycle.Instance.IsServerRunning || !NetworkLifecycle.Instance.IsClientRunning) + { + Multiplayer.LogWarning("APIProvider.UnregisterPaintTheme() called when server or client is not running"); + return; + } + + PaintThemeLookup.Instance.UnregisterTheme(theme); + } + + #region Task Serialisation + public bool RegisterTaskType(TaskType taskType) + where TGameTask : Task + where TNetworkData : TaskNetworkData, new() + { + return TaskNetworkDataFactory.RegisterTaskType(taskType); + } + + public bool UnregisterTaskType(TaskType taskType) where TGameTask : Task + { + return TaskNetworkDataFactory.UnregisterTaskType(taskType); + } + + public TaskNetworkData[] ConvertTasks(IEnumerable tasks) + { + return TaskNetworkDataFactory.ConvertTasks(tasks); + } + + public TaskNetworkData ConvertTask(Task task) + { + return TaskNetworkDataFactory.ConvertTask(task); + } + + public TaskNetworkData ConvertTask(TaskType type) + { + return TaskNetworkDataFactory.ConvertTask(type); + } + + #endregion + + #region Class Helpers + + internal APIProvider() + { + NetworkLifecycle.Instance.OnTick += OnTickInternal; + } + + internal void Dispose() + { + NetworkLifecycle.Instance.OnTick -= OnTickInternal; + } + + internal void OnTickInternal(uint tick) + { + OnTick?.Invoke(tick); + } + + #endregion +} diff --git a/Multiplayer/API/ClientAPIProvider.cs b/Multiplayer/API/ClientAPIProvider.cs new file mode 100644 index 00000000..7bbbe4a7 --- /dev/null +++ b/Multiplayer/API/ClientAPIProvider.cs @@ -0,0 +1,107 @@ +using MPAPI.Interfaces; +using MPAPI.Interfaces.Packets; +using MPAPI.Types; +using Multiplayer.Components.Networking.Player; +using Multiplayer.Networking.Managers.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using static UnityModManagerNet.UnityModManager; + +namespace Multiplayer.API +{ + public class ClientAPIProvider : IClient + { + private readonly Dictionary _playerWrapperCache = []; + private readonly NetworkClient client; + + public event Action OnPlayerConnected; + public event Action OnPlayerDisconnected; + + public void RegisterReadyBlock(ModInfo modInfo) + { + client.RegisterReadyBlock(modInfo.DisplayName); + } + + public void CancelReadyBlock(ModInfo modInfo) + { + client.CancelReadyBlock(modInfo.DisplayName); + } + + #region Client Properties + public byte PlayerId => client.PlayerId; + public IReadOnlyCollection Players => client.ClientPlayerManager.Players.Select(GetWrapper).ToList().AsReadOnly(); + public int PlayerCount => client.ClientPlayerManager.Players.Count + 1; // add 1 for local player + + public IPlayer GetPlayer(byte playerId) + { + _playerWrapperCache.TryGetValue(playerId, out var player); + return player; + } + + public bool IsConnected => client.IsRunning; + + public int Ping => client.Ping; + + #endregion + + #region Packet API + public void RegisterPacket(ClientPacketHandler handler) where T : class, IPacket, new() + { + client.RegisterExternalPacket(handler); + } + public void RegisterSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new() + { + client.RegisterExternalSerializablePacket(handler); + } + + public void SendPacketToServer(T packet, bool reliable = true) where T : class, IPacket, new() + { + client.SendExternalPacketToServer(packet, reliable); + } + + public void SendSerializablePacketToServer(T packet, bool reliable = true) where T : class, ISerializablePacket, new() + { + client.SendExternalSerializablePacketToServer(packet, reliable); + } + #endregion + + #region Class Helpers + internal ClientAPIProvider(NetworkClient clientInstance) + { + this.client = clientInstance; + + client.ClientPlayerManager.OnPlayerConnected += OnPlayerConnectedInternal; + client.ClientPlayerManager.OnPlayerDisconnected += OnPlayerDisconnectedInternal; + } + + internal void Dispose() + { + client.ClientPlayerManager.OnPlayerConnected -= OnPlayerConnectedInternal; + client.ClientPlayerManager.OnPlayerDisconnected -= OnPlayerDisconnectedInternal; + } + + + private ClientPlayerWrapper GetWrapper(NetworkedPlayer networkedPlayer) + { + if (!_playerWrapperCache.TryGetValue(networkedPlayer.PlayerId, out var wrapper)) + { + wrapper = new ClientPlayerWrapper(networkedPlayer); + _playerWrapperCache[networkedPlayer.PlayerId] = wrapper; + } + return wrapper; + } + + private void OnPlayerConnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) + { + OnPlayerConnected?.Invoke(GetWrapper(networkedPlayer)); + } + + private void OnPlayerDisconnectedInternal(Components.Networking.Player.NetworkedPlayer networkedPlayer) + { + OnPlayerDisconnected?.Invoke(GetWrapper(networkedPlayer)); + _playerWrapperCache.Remove(networkedPlayer.PlayerId); + } + #endregion + } +} diff --git a/Multiplayer/API/ClientPlayerWrapper.cs b/Multiplayer/API/ClientPlayerWrapper.cs new file mode 100644 index 00000000..7cd79f2c --- /dev/null +++ b/Multiplayer/API/ClientPlayerWrapper.cs @@ -0,0 +1,31 @@ +using MPAPI.Interfaces; +using Multiplayer.Components.Networking.Player; +using UnityEngine; + +namespace Multiplayer.API; + +public class ClientPlayerWrapper : IPlayer +{ + private readonly NetworkedPlayer _networkedPlayer; + private readonly bool _isHost; + + public ClientPlayerWrapper(NetworkedPlayer networkedPlayer, bool isHost = false) + { + _networkedPlayer = networkedPlayer; + _isHost = isHost; + } + + public byte PlayerId => _networkedPlayer.PlayerId; + public string Username + { + get => _networkedPlayer.Username; + set => _networkedPlayer.Username = value; + } + public Vector3 Position => _networkedPlayer.transform.position; + public float RotationY => _networkedPlayer.transform.rotation.eulerAngles.y; + public bool IsLoaded => true; // If we have the object, it's loaded + public bool IsHost => _isHost; + public int Ping => _networkedPlayer.GetPing(); + public bool IsOnCar => _networkedPlayer.IsOnCar; + public TrainCar OccupiedCar => _networkedPlayer.OccupiedCar; +} diff --git a/Multiplayer/API/ExternalSerializablePacketWrapper.cs b/Multiplayer/API/ExternalSerializablePacketWrapper.cs new file mode 100644 index 00000000..f5b7d404 --- /dev/null +++ b/Multiplayer/API/ExternalSerializablePacketWrapper.cs @@ -0,0 +1,56 @@ +using LiteNetLib.Utils; +using MPAPI.Interfaces.Packets; +using System.IO; + +namespace Multiplayer.API; + +/// +/// Wrapper for external serializable packets to integrate with LiteNetLib +/// +/// The packet type +public class ExternalSerializablePacketWrapper : INetSerializable where T : class, ISerializablePacket, new() +{ + const int COMPRESSION_THRESHOLD = 1024; + + public T Packet { get; set; } + + public void Serialize(NetDataWriter writer) + { + byte[] data; + + using var memoryStream = new MemoryStream(); + using var binaryWriter = new BinaryWriter(memoryStream); + + Packet.Serialize(binaryWriter); + + data = memoryStream.ToArray(); + + bool shouldCompress = memoryStream.Length >= COMPRESSION_THRESHOLD; + writer.Put(shouldCompress); + + if (shouldCompress) + { + var lenBefore = data.Length; + data = PacketCompression.Compress(data); + + Multiplayer.LogDebug(() => $"ExternalSerializablePacketWrapper<{typeof(T).Name}>: Compressed {lenBefore} to {data.Length} bytes"); + } + + writer.PutBytesWithLength(data); + } + + public void Deserialize(NetDataReader reader) + { + bool isCompressed = reader.GetBool(); + var data = reader.GetBytesWithLength(); + + if (isCompressed) + data = PacketCompression.Decompress(data); + + using var memoryStream = new MemoryStream(data); + using var binaryReader = new BinaryReader(memoryStream); + + Packet = new T(); + Packet.Deserialize(binaryReader); + } +} diff --git a/Multiplayer/API/ModCompatibilityManager.cs b/Multiplayer/API/ModCompatibilityManager.cs new file mode 100644 index 00000000..ce14ed01 --- /dev/null +++ b/Multiplayer/API/ModCompatibilityManager.cs @@ -0,0 +1,201 @@ +using DV.JObjectExtstensions; +using DV.Utils; +using JetBrains.Annotations; +using MPAPI.Types; +using Multiplayer.Components.MainMenu; +using Multiplayer.Networking.Data; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityModManagerNet; + +namespace Multiplayer.API; + +public class ModCompatibilityManager : SingletonBehaviour +{ + private const string JSON_FILE = "info.json"; + private const string JSON_MP_COMPAT_KEY = "MultiplayerCompatibility"; + private static readonly Dictionary _modCompatibility = []; + + protected override void Awake() + { + base.Awake(); + + DontDestroyOnLoad(this); + + //Register ourselves + RegisterCompatibility("Multiplayer", MultiplayerCompatibility.All); + + //we don't care if the client does/doesn't have these mods + RegisterCompatibility("RuntimeUnityEditor", MultiplayerCompatibility.Client); + RegisterCompatibility("BookletOrganizer", MultiplayerCompatibility.Host); + RegisterCompatibility("RemoteDispatch", MultiplayerCompatibility.Client); + RegisterCompatibility("CommsRadioAPI", MultiplayerCompatibility.Client); + RegisterCompatibility("DVCustomCargo", MultiplayerCompatibility.All); + RegisterCompatibility("DVDiscordPresenceMod", MultiplayerCompatibility.Client); + RegisterCompatibility("DVLangHelper", MultiplayerCompatibility.Client); + RegisterCompatibility("LightingOverhaul", MultiplayerCompatibility.Client); + RegisterCompatibility("dv-improved-job-overview", MultiplayerCompatibility.Client); + RegisterCompatibility("dv_f_spammer", MultiplayerCompatibility.Client); + + //Json entries will override hardcoded entries + ReadModJsons(); + + //Hardcoded and json entries will be overridden by API calls + } + + public void RegisterCompatibility(string modId, MultiplayerCompatibility compatibility) + { + Multiplayer.LogDebug(() => $"RegisterCompatibility({modId}, {compatibility})"); + + if (!string.IsNullOrEmpty(modId)) + _modCompatibility[modId] = compatibility; + } + + private void ReadModJsons() + { + foreach (var modEntry in UnityModManager.modEntries) + { + var jsonPath = Path.Combine(modEntry.Path, JSON_FILE); + + if (File.Exists(jsonPath)) + { + try + { + var json = File.ReadAllText(jsonPath); + var jobj = JObject.Parse(json); + var compatStr = jobj.GetString(JSON_MP_COMPAT_KEY); + + var parsed = Enum.TryParse(compatStr, out MultiplayerCompatibility compatibility); + + Multiplayer.LogDebug(() => $"Mod '{modEntry.Info.DisplayName}' ({modEntry.Info.Id}) has MP mod compatibility entry \'{compatStr}\', parses to: {compatibility}"); + + if (parsed) + RegisterCompatibility(modEntry.Info.Id, compatibility); + } + catch (Exception e) + { + Multiplayer.LogException($"Failed to parse mod entry {modEntry.Info.Id}", e); + } + } + else + { + Multiplayer.LogWarning($"No json found for {modEntry.Info.Id}"); + } + } + } + + public bool TryGetCompatibility(string modId, out MultiplayerCompatibility compatibility) + { + return _modCompatibility.TryGetValue(modId, out compatibility); + } + + public MultiplayerCompatibility GetCompatibility(ModInfo mod) + { + if (TryGetCompatibility(mod.Id, out var compatibility)) + return compatibility; + + return MultiplayerCompatibility.Undefined; + } + + public ModValidationResult ValidateClientMods(ModInfo[] clientMods) + { + var localMods = GetLocalMods(); + var localModIds = localMods.Select(l => l.Id); + + var clientModIds = clientMods.Select(c => c.Id); + + List missing = clientMods.Where(c => !localModIds.Contains(c.Id)).ToList(); + List extra = localMods.Where(l => !clientModIds.Contains(l.Id)).ToList(); + + bool valid = (missing.Count == 0) && (extra.Count == 0); + + return new() + { + IsValid = valid, + Missing = missing, + Extra = extra + }; + } + + /// + /// Checks if any incompatible mods are enabled and generates an message box to alert the user + /// + /// True if incompatible mods have been found + public bool CheckModCompatibility() + { + List incompatible = []; + + foreach (var modInfo in ModInfo.FromModEntries(UnityModManager.modEntries)) + { + if (TryGetCompatibility(modInfo.Id, out var compatibility)) + { + if (compatibility == MultiplayerCompatibility.Incompatible) + incompatible.Add(modInfo.Id); + } + } + + if (incompatible.Count == 0) + return false; + + var message = $"{Locale.MAIN_MENU__INCOMPATIBLE_MODS} {string.Join(", ", incompatible)}"; + + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + + return true; + } + + // Returns a list of mods installed and enabled, filtered for mods that are required for hosts and clients + public ModInfo[] GetLocalMods() + { + List localMods = []; + + foreach (var modInfo in ModInfo.FromModEntries(UnityModManager.modEntries)) + { + if (TryGetCompatibility(modInfo.Id, out var compatibility)) + { + // Only include mods that are relevant for client validation + switch (compatibility) + { + case MultiplayerCompatibility.Undefined: + case MultiplayerCompatibility.All: + //undefined and "All" mods are required by all clients + localMods.Add(modInfo); + break; + + case MultiplayerCompatibility.Incompatible: + //There shouldn't be any at this stage + localMods.Add(modInfo); + break; + + case MultiplayerCompatibility.Host: + case MultiplayerCompatibility.Client: + // Not required, should have no impact on game play + break; + } + } + else + { + // No compatibility info - include for validation (safe default) + Multiplayer.LogWarning($"No compatibility info for mod {modInfo.Id}, including in validation"); + localMods.Add(modInfo); + } + } + return localMods.ToArray(); + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(ModCompatibilityManager)}]"; + } +} + +public class ModValidationResult +{ + public bool IsValid { get; set; } + public List Missing { get; set; } = []; + public List Extra { get; set; } = []; +} diff --git a/Multiplayer/API/NetIdProvider.cs b/Multiplayer/API/NetIdProvider.cs new file mode 100644 index 00000000..9e5e3043 --- /dev/null +++ b/Multiplayer/API/NetIdProvider.cs @@ -0,0 +1,119 @@ +using DV.CabControls; +using DV.Customization.Paint; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using JetBrains.Annotations; +using MPAPI.Interfaces; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; +using System; +using System.Collections.Generic; + +namespace Multiplayer.API; + +public delegate bool TryGetNetIdDelegate(T obj, out ushort netId) where T : class; +public delegate bool TryGetObjectDelegate(ushort netId, out T obj) where T : class; + +public delegate bool TryGetUIntNetIdDelegate(T obj, out uint netId) where T : class; +public delegate bool TryGetObjectUIntDelegate(uint netId, out T obj) where T : class; + +internal class NetIdProvider : SingletonBehaviour, INetIdProvider +{ + private readonly Dictionary handlers = []; + + protected override void Awake() + { + base.Awake(); + RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); + RegisterHandler(NetworkedTrainCar.TryGetNetId, NetworkedTrainCar.TryGet); + + RegisterHandler(CargoTypeLookup.Instance.TryGetNetId, CargoTypeLookup.Instance.TryGet); + RegisterHandler(PaintThemeLookup.Instance.TryGetNetId, PaintThemeLookup.Instance.TryGet); + + RegisterHandler(NetworkedJunction.TryGetNetId, NetworkedJunction.TryGet); + RegisterHandler(NetworkedTurntable.TryGetNetId, NetworkedTurntable.TryGet); + RegisterHandler(NetworkedRailTrack.TryGetNetId, NetworkedRailTrack.TryGet); + + RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + RegisterHandler(NetworkedStationController.TryGetNetId, NetworkedStationController.TryGet); + + RegisterHandler(WarehouseMachineLookup.TryGetNetId, WarehouseMachineLookup.TryGet); + RegisterHandler(NetworkedWarehouseMachineController.TryGetNetId, NetworkedWarehouseMachineController.TryGet); + + RegisterHandler(NetworkedJob.TryGetNetId, NetworkedJob.TryGetJob); + RegisterHandler(NetworkedTask.TryGetNetId, NetworkedTask.TryGet); + + RegisterHandler(NetworkedItem.TryGetNetId, NetworkedItem.GetItem); + } + + public void RegisterHandler(TryGetNetIdDelegate tryGetNetId, TryGetObjectDelegate tryGetObject) where T : class + { + handlers[typeof(T)] = (tryGetNetId, tryGetObject); + } + + public void RegisterHandler(TryGetUIntNetIdDelegate tryGetNetId, TryGetObjectUIntDelegate tryGetObject) where T : class + { + handlers[typeof(T)] = (tryGetNetId, tryGetObject); + } + + public bool TryGetNetId(T obj, out ushort netId) where T : class + { + netId = 0; + + if (obj == null) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetNetIdDelegate tryGetNetId, TryGetObjectDelegate _)) + return tryGetNetId(obj, out netId); + + return false; + } + + public bool TryGetObject(ushort netId, out T obj) where T : class + { + obj = null; + + if (netId == 0) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetNetIdDelegate _, TryGetObjectDelegate tryGetObject)) + return tryGetObject(netId, out obj); + + return false; + } + + public bool TryGetNetId(T obj, out uint netId) where T : class + { + netId = 0; + + if (obj == null) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetUIntNetIdDelegate tryGetNetId, TryGetObjectUIntDelegate _)) + return tryGetNetId(obj, out netId); + + return false; + } + + public bool TryGetObject(uint netId, out T obj) where T : class + { + obj = null; + + if (netId == 0) + return false; + + if (handlers.TryGetValue(typeof(T), out var handler) && handler is (TryGetUIntNetIdDelegate _, TryGetObjectUIntDelegate tryGetObject)) + return tryGetObject(netId, out obj); + + return false; + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(NetIdProvider)}]"; + } +} diff --git a/Multiplayer/API/ServerAPIProvider.cs b/Multiplayer/API/ServerAPIProvider.cs new file mode 100644 index 00000000..cc4580f6 --- /dev/null +++ b/Multiplayer/API/ServerAPIProvider.cs @@ -0,0 +1,200 @@ +using MPAPI.Interfaces; +using MPAPI.Interfaces.Packets; +using MPAPI.Types; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.TransportLayers; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Multiplayer.API; + +public class ServerAPIProvider : IServer +{ + private readonly Dictionary _playerWrapperCache = []; + private readonly NetworkServer server; + + public event Action OnPlayerConnected; + public event Action OnPlayerDisconnected; + public event Action OnPlayerReady; + + #region Server Properties + + public int PlayerCount => server.PlayerCount; + + public IReadOnlyCollection Players => server.ServerPlayers.Select(GetWrapper).ToList().AsReadOnly(); + + public IPlayer GetPlayer(byte PlayerId) + { + _playerWrapperCache.TryGetValue(PlayerId, out var player); + return player; + } + #endregion + + #region Packet API + public void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new() + { + server.RegisterExternalPacket(handler); + } + public void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new() + { + server.RegisterExternalSerializablePacket(handler); + } + + + public void SendPacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, IPacket, new() + { + ITransportPeer peer = null; + + if (excludePlayer != null) + peer = GetPeerFromPlayer(excludePlayer, $"SendPacketToAll<{typeof(T).Name}>"); + + server.SendExternalPacketToAll(packet, reliable, peer, excludeSelf); + } + + public void SendSerializablePacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new() + { + ITransportPeer peer = null; + + if(excludePlayer != null) + peer = GetPeerFromPlayer(excludePlayer, $"SendSerializablePacketToAll<{typeof(T).Name}>"); + + server.SendExternalSerializablePacketToAll(packet, reliable, peer, excludeSelf); + } + + public void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new() + { + var peer = GetPeerFromPlayer(player, $"SendPacketToPlayer<{typeof(T).Name}>"); + + if (peer != null) + server.SendExternalPacketToPlayer(packet, peer, reliable); + } + + public void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new() + { + var peer = GetPeerFromPlayer(player, $"SendSerializablePacketToPlayer<{typeof(T).Name}>"); + + if (peer != null) + server.SendExternalSerializablePacketToPlayer(packet, peer, reliable); + } + #endregion + + #region Server Util + public float AnyPlayerSqrMag(GameObject item) => DvExtensions.AnyPlayerSqrMag(item); + + public float AnyPlayerSqrMag(Vector3 anchor) => DvExtensions.AnyPlayerSqrMag(anchor); + #endregion + + #region Chat + public void SendServerChatMessage(string message, IPlayer excludePlayer = null) + { + var excludedServerPlayer = GetServerPlayerFromIPlayer(excludePlayer); + if (excludedServerPlayer != null) + server.ChatManager.ServerMessage(message, null, excludedServerPlayer); + } + + public void SendWhisperChatMessage(string message, IPlayer player) + { + var serverPlayer = GetServerPlayerFromIPlayer(player); + if (serverPlayer != null) + server.SendWhisper(message, serverPlayer); + } + + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallback callback) + { + ChatCommandCallbackInternal internalCallback = (message, serverPlayer) => + { + var playerWrapper = GetWrapper(serverPlayer); + callback(message, playerWrapper); + }; + + return server.ChatManager.RegisterChatCommand(commandLong, commandShort, helpMessage, internalCallback); + } + + public void RegisterChatFilter(ChatFilterDelegate callback) + { + ChatFilterDelegateInternal internalCallback = (ref string message, ServerPlayer serverPlayer) => + { + var playerWrapper = GetWrapper(serverPlayer); + return callback(ref message, playerWrapper); + }; + + server.ChatManager.RegisterChatFilter(internalCallback); + } + #endregion + + #region Class Helpers + internal ServerAPIProvider(NetworkServer serverInstance) + { + this.server = serverInstance; + + server.PlayerConnected += OnPlayerConnectedInternal; + server.PlayerDisconnected += OnPlayerDisconnectedInternal; + server.PlayerReady += OnPlayerReadyInternal; + } + + private ServerPlayerWrapper GetWrapper(ServerPlayer serverPlayer) + { + if (!_playerWrapperCache.TryGetValue(serverPlayer.PlayerId, out var wrapper)) + { + wrapper = new ServerPlayerWrapper(serverPlayer); + _playerWrapperCache[serverPlayer.PlayerId] = wrapper; + } + return wrapper; + } + + private ITransportPeer GetPeerFromPlayer(IPlayer player, string operationName) + { + if (player == null) + { + server.LogDebug(() => $"{operationName}: Player is null"); + return null; + } + + if (player is ServerPlayerWrapper playerWrapper) + { + return playerWrapper.Peer; + } + + server.LogWarning($"{operationName}: Player '{player.Username}' is not a ServerPlayerWrapper (got {player.GetType().Name})"); + return null; + } + + private ServerPlayer GetServerPlayerFromIPlayer(IPlayer player) + { + if (player == null) + return null; + + if (player is ServerPlayerWrapper wrapper) + return wrapper._serverPlayer; + + server.LogWarning($"GetServerPlayerFromIPlayer: Player '{player.Username}' is not a ServerPlayerWrapper (got {player.GetType().Name})"); + return null; + } + + internal void Dispose() + { + server.PlayerConnected -= OnPlayerConnectedInternal; + server.PlayerDisconnected -= OnPlayerDisconnectedInternal; + } + + private void OnPlayerConnectedInternal(ServerPlayer serverPlayer) + { + OnPlayerConnected?.Invoke(GetWrapper(serverPlayer)); + } + + private void OnPlayerDisconnectedInternal(ServerPlayer serverPlayer) + { + OnPlayerDisconnected?.Invoke(GetWrapper(serverPlayer)); + _playerWrapperCache.Remove(serverPlayer.PlayerId); + } + + private void OnPlayerReadyInternal(ServerPlayer serverPlayer) + { + OnPlayerReady?.Invoke(GetWrapper(serverPlayer)); + } + #endregion +} diff --git a/Multiplayer/API/ServerPlayerWrapper.cs b/Multiplayer/API/ServerPlayerWrapper.cs new file mode 100644 index 00000000..93eeb173 --- /dev/null +++ b/Multiplayer/API/ServerPlayerWrapper.cs @@ -0,0 +1,44 @@ +using MPAPI.Interfaces; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.TransportLayers; +using UnityEngine; + +namespace Multiplayer.API; + +public class ServerPlayerWrapper : IPlayer +{ + internal readonly ServerPlayer _serverPlayer; + private readonly bool _isHost; + + public ServerPlayerWrapper(ServerPlayer serverPlayer) + { + _serverPlayer = serverPlayer; + _isHost = NetworkLifecycle.Instance?.IsHost(serverPlayer) ?? false; + } + + public byte PlayerId => _serverPlayer.PlayerId; + + public string Username + { + get => _serverPlayer.Username; + set => _serverPlayer.Username = value; + } + + public Vector3 Position => _serverPlayer.WorldPosition; + public float RotationY => _serverPlayer.WorldRotationY; + public bool IsLoaded => _serverPlayer.IsLoaded; + public bool IsHost => _isHost; + public int Ping => 0; // Server doesn't track ping for players + public bool IsOnCar => _serverPlayer.CarId != 0; + public TrainCar OccupiedCar => GetOccupiedCar(); + + internal TrainCar GetOccupiedCar() + { + NetworkedTrainCar.TryGet(_serverPlayer.CarId, out TrainCar trainCar); + return trainCar; + } + + internal ITransportPeer Peer => _serverPlayer.Peer; +} diff --git a/Multiplayer/Components/IdMonoBehaviour.cs b/Multiplayer/Components/IdMonoBehaviour.cs index f8fa3c6a..732e4339 100644 --- a/Multiplayer/Components/IdMonoBehaviour.cs +++ b/Multiplayer/Components/IdMonoBehaviour.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using LiteNetLib.Utils; using Multiplayer.Components.Networking; using Multiplayer.Utils; using UnityEngine; @@ -9,7 +8,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 +31,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 00000000..1967d507 --- /dev/null +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -0,0 +1,485 @@ +using DV; +using DV.Common; +using DV.Localization; +using DV.Platform.Steam; +using DV.UI; +using DV.UI.PresetEditors; +using DV.UIFramework; +using Multiplayer.API; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Util; +using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; +using Multiplayer.Utils; +using System; +using System.Linq; +using System.Reflection; +using TMPro; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.UI; + +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; + Selector gameVisibility; + ButtonDV startButton; + + public ISaveGame saveGame; + public UIStartGameData startGameData; + public AUserProfileProvider userProvider; + public AScenarioProvider scenarioProvider; + LauncherController lcInstance; + + public Action continueCareerRequested; + + private bool incompatibleMods = true; + #region setup + + public void Awake() + { + Multiplayer.Log("HostGamePane Awake()"); + + CleanUI(); + BuildUI(); + ValidateInputs(null); + } + + public void Start() + { + Multiplayer.Log("HostGamePane Started"); + + if (DVSteamworks.Success) + return; + + Multiplayer.Log($"Steam not detected, prompt for restart."); + MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", () => { }); + } + + public void OnEnable() + { + this.SetupListeners(true); + + incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); + ValidateInputs(null); + } + + // 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 selectorPrefab = goMMC.FindChildByName("Crosshair").gameObject; + if (selectorPrefab == null) + { + Multiplayer.LogError("selectorPrefab not found!"); + return; + } + + GameObject sliderPrefab = goMMC.FindChildByName("Field Of View").gameObject; + if (sliderPrefab == null) + { + Multiplayer.LogError("Field Of View 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; + + /* + * Server name field + */ + GameObject go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Server Name"; + 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(); + + /* + * Server password field + */ + 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(); + + /* + * Server visibility field + */ + selectorPrefab.SetActive(false); + go = GameObject.Instantiate(selectorPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + selectorPrefab.SetActive(true); + gameVisibility = go.GetOrAddComponent(); + + //clean-up + + if (gameVisibility.labelTMPro?.gameObject.TryGetComponent(out var loc) ?? false) + GameObject.DestroyImmediate(loc); + if (gameVisibility.labelTMPro?.gameObject.TryGetComponent(out var loc2) ?? false) + GameObject.DestroyImmediate(loc2); + + DestroyImmediate(go.GetComponent()); + + go.name = "Visibility"; + gameVisibility.initialized = false; + + gameVisibility.LocalizedLabel = true; + gameVisibility.SetLabel(Locale.SERVER_HOST_VISIBILITY_KEY); + gameVisibility.labelTMPro.GetComponent().key = Locale.SERVER_HOST_VISIBILITY_KEY; + + gameVisibility.LocalizedValues = true; + gameVisibility.SetValues(Locale.SERVER_HOST_VISIBILITY_MODES.ToList()); + gameVisibility.SetSelectedIndex(3); + + go.SetActive(true); + go.ResetTooltip(); + + gameVisibility.ToggleInteractable(true); + + /* + * Server details field + */ + 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; + + //Divider + go = GameObject.Instantiate(dividerPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Divider"; + + /* + * Server max players field + */ + sliderPrefab.SetActive(false); + go = GameObject.Instantiate(sliderPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + sliderPrefab.SetActive(true); + maxPlayers = go.GetComponent(); + + go.name = "Max Players"; + var labelGo = go.FindChildByName("[text label]"); + + if (labelGo?.gameObject.TryGetComponent(out loc) ?? false) + GameObject.DestroyImmediate(loc); + + DestroyImmediate(go.GetComponent()); + + labelGo.GetComponent().key = Locale.SERVER_HOST_MAX_PLAYERS_KEY; + go.ResetTooltip(); + //labelGo.GetComponent().UpdateLocalization(); + + maxPlayers.stepIncrement = 1; + maxPlayers.minValue = MIN_PLAYERS; + maxPlayers.maxValue = MAX_PLAYERS; + maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers, MIN_PLAYERS, MAX_PLAYERS); + go.SetActive(true); + maxPlayers.interactable = true; + + /* + * Server port field + */ + 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 = DEFAULT_PORT.ToString(); + port.text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); + + /* + * Start Game button + */ + 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(OnStartClick); + } + + 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 _) + { + bool valid = true; + + if (incompatibleMods) + valid = false; + + if (!DVSteamworks.Success) + valid = false; + + if (serverName.text.Trim() == "" || serverName.text.Length > MAX_SERVER_NAME_LEN) + valid = false; + + if (port.text != "") + { + if (!int.TryParse(port.text, out int portNum) || portNum < MIN_PORT || portNum > MAX_PORT) + valid = false; + } + + if (port.text == "" && (Multiplayer.Settings.Port < MIN_PORT || Multiplayer.Settings.Port > MAX_PORT)) + valid = false; + + startButton.ToggleInteractable(valid); + } + + private void OnStartClick() + { + + 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.Visibility = (ServerVisibility)gameVisibility.SelectedIndex; + + 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; + + // final check before we start the server + var requiredMods = ModCompatibilityManager.Instance.GetLocalMods(); + if (requiredMods == null) + { + incompatibleMods = true; + ValidateInputs(null); + return; + } + + serverData.RequiredMods = requiredMods; + serverData.GameVersion = MainMenuControllerPatch.MenuProvider.BuildVersionString; + 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.Visibility = serverData.Visibility; + 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 02a6d6b2..2dea38f7 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 be420685..00000000 --- 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 00000000..ddbba69d --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -0,0 +1,35 @@ +using Multiplayer.Networking.Data; +using System; + +namespace Multiplayer.Components.MainMenu; + +public enum ServerVisibility : int +{ + Private = 0, + Friends = 1, + Public = 2 +} + +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; } + ModInfo[] RequiredMods { get; set; } + string GameVersion { get; set; } + string MultiplayerVersion { get; set; } + string ServerDetails { get; set; } + int Ping {get; set; } + ServerVisibility Visibility { 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 00000000..170caabd --- /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/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs new file mode 100644 index 00000000..8cbef966 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -0,0 +1,137 @@ + +using Multiplayer.Components.UI.Controls; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu.ServerBrowser +{ + public class ServerBrowserElement : MPViewElement + { + public override bool IsPlaceholder => false; + + private TextMeshProUGUI serverName; + 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_PADDING_X = 10; + + 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; + + public override void Awake() + { + // Find and assign TextMeshProUGUI components for displaying server details + serverName = 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(); + + RectTransform nameRT = serverName.rectTransform; + + // Align player count + RectTransform playerCountRT = playerCount.rectTransform; + playerCountRT.anchorMin = new Vector2(0, 0.5f); + playerCountRT.anchorMax = new Vector2(0, 0.5f); + playerCountRT.pivot = new Vector2(0, 0.5f); + + float nameWidth = nameRT.sizeDelta.x; + playerCountRT.anchoredPosition = new Vector2(nameRT.position.x + nameWidth, nameRT.anchoredPosition.y); + + // Align ping + RectTransform pingRT = ping.rectTransform; + pingRT.anchorMin = new Vector2(0, 0.5f); + pingRT.anchorMax = new Vector2(0, 0.5f); + pingRT.pivot = new Vector2(0, 0.5f); + + RectTransform parentRT = transform as RectTransform; + float pingX = parentRT.rect.width - PING_WIDTH - PING_PADDING_X; + pingRT.anchoredPosition = new Vector2(pingX, nameRT.anchoredPosition.y); + pingRT.sizeDelta = new Vector2(PING_WIDTH, pingRT.sizeDelta.y); + 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 += (pingRT.position.x - LANpos.x - LANSize.x) / 2; + goIconLAN.transform.localPosition = LANpos; + iconLAN = goIconLAN.GetComponent(); + iconLAN.sprite = Multiplayer.AssetIndex.lanIcon; + } + + } + + public override void SetData(IServerBrowserGameDetails data) + { + // 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() + { + //Multiplayer.LogDebug(() => $"UpdateView() serverName: {data.Name}, ping: {data.Ping}"); + + // Update the text fields with the data from the server + serverName.text = data.Name; + playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; + + ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; + + // 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) + { + return ping switch + { + PING_THRESHOLD_NONE => PING_COLOR_UNKNOWN, + < PING_THRESHOLD_EXCELLENT => PING_COLOR_EXCELLENT, + < PING_THRESHOLD_GOOD => PING_COLOR_GOOD, + < PING_THRESHOLD_HIGH => PING_COLOR_HIGH, + _ => PING_COLOR_POOR, + }; + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs new file mode 100644 index 00000000..ddbef4c9 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -0,0 +1,36 @@ +using DV.UI; +using Multiplayer.Components.UI.Controls; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu.ServerBrowser; + +[RequireComponent(typeof(ContentSizeFitter))] +[RequireComponent(typeof(VerticalLayoutGroup))] +public class ServerBrowserGridView : MPGridView +{ + + protected override void Awake() + { + showPlaceholderWhenEmpty = true; + + //copy the copy + viewElementPrefab.SetActive(false); + placeholderElementPrefab = Instantiate(viewElementPrefab); + + //swap controllers + Destroy(viewElementPrefab.GetComponent()); + GameObject.Destroy(placeholderElementPrefab.GetComponent()); + + viewElementPrefab.AddComponent(); + placeholderElementPrefab.AddComponent(); + + viewElementPrefab.name = "prefabSBElement"; + placeholderElementPrefab.name = "prefabSBPlaceholderElement"; + + viewElementPrefab.SetActive(true); + placeholderElementPrefab.SetActive(true); + + base.Awake(); + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs new file mode 100644 index 00000000..1b58419c --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserPlaceholderElement.cs @@ -0,0 +1,48 @@ +using DV.UI; +using DV.UIFramework; +using DV.Localization; +using Multiplayer.Utils; +using UnityEngine; +using Multiplayer.Components.UI.Controls; + +namespace Multiplayer.Components.MainMenu.ServerBrowser +{ + public class ServerBrowserPlaceholderElement : MPViewElement + { + public override bool IsPlaceholder => true; + + public override void Awake() + { + // Find and assign TextMeshProUGUI components for displaying server details + GameObject networkNameGO = this.FindChildByName("name [noloc]"); + + 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) + { + //do nothing + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs new file mode 100644 index 00000000..81a07683 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -0,0 +1,1274 @@ +using DV.Localization; +using DV.Platform.Steam; +using DV.UI; +using DV.UI.Manual; +using DV.UIFramework; +using DV.Utils; +using LiteNetLib; +using MPAPI.Types; +using Multiplayer.API; +using Multiplayer.Components.MainMenu.ServerBrowser; +using Multiplayer.Components.Networking; +using Multiplayer.Components.UI.Controls; +using Multiplayer.Components.Util; +using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; +using Multiplayer.Utils; +using Steamworks; +using Steamworks.Data; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using Color = UnityEngine.Color; + +namespace Multiplayer.Components.MainMenu; + +public class ServerBrowserPane : MonoBehaviour +{ + private const string FORMAT_ALPHA = ""; + + private enum ConnectionState + { + NotConnected, + JoiningLobby, + AwaitingPassword, + AttemptingSteamRelay, + AttemptingIPv6, + AttemptingIPv6Punch, + AttemptingIPv4, + AttemptingIPv4Punch, + Connected, + Failed, + Aborted + } + + private const int MAX_PORT_LEN = 5; + private const int MIN_PORT = 1024; + private const int MAX_PORT = 49151; + + // Gridview variables + private ServerBrowserGridView serverGridView; + 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; + private GameObject navigationButtonPrefab; + private Transform detailsContent; + private CollapsibleElement elementRequiredMods; + private CollapsibleElement elementExtraMods; + + // 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; + + private bool incompatibleMods = true; + + #region setup + + public void Awake() + { + Multiplayer.Log("MultiplayerPane Awake()"); + joinedLobby?.Leave(); + joinedLobby = null; + + CleanUI(); + BuildUI(); + + SetupServerBrowser(); + } + + public void OnEnable() + { + //ensure no incompatible mods are loaded + incompatibleMods = ModCompatibilityManager.Instance.CheckModCompatibility(); + + this.SetupListeners(true); + + 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(); + OnSelectedIndexChanged(serverGridView); //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 && connectionState == ConnectionState.NotConnected) + { + //For invites/requests + Multiplayer.Log($"Player invite initiated/request"); + + if (lobbyToJoin.Value.Id.IsValid) + { + direct = false; + var _ = JoinLobby((Lobby)lobbyToJoin); + } + else + { + Multiplayer.LogWarning("Received invalid lobby invite"); + lobbyToJoin = null; + } + } + } + + public void Start() + { + if (DVSteamworks.Success) + return; + + Multiplayer.Log($"Steam not detected, prompt for restart."); + MainMenuThingsAndStuff.Instance.ShowOkPopup("Steam not detected. Please restart the game with Steam running", () => { }); + } + + 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)); + detailsContent = content.transform; + detailsContent.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; + contentRT.localPosition = new Vector3(contentRT.localPosition.x + 10, contentRT.localPosition.y, contentRT.localPosition.z); + + // Create TextMeshProUGUI object + GameObject textGO = new("Details Text", typeof(TextMeshProUGUI)); + textGO.transform.SetParent(contentRT.transform, false); + 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."; + + SetupModsGroup(); + + // 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(); + serverGridView = GridviewGO.AddComponent(); + + //grab the original prefab + slgv.viewElementPrefab.SetActive(false); + serverGridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); + slgv.viewElementPrefab.SetActive(true); + + //Remove original controller + GameObject.Destroy(slgv); + + //Don't forget to re-enable! + GridviewGO.SetActive(true); + serverGridView.Clear(); + } + + private void SetupModsGroup() + { + ManualController manualController = MainMenuControllerPatch.MainMenuControllerInstance.GetComponentInChildren(true); + if (manualController == null) + { + Multiplayer.LogError("SetupModsGroup() ManualController not found"); + return; + } + + navigationButtonPrefab = manualController.navigationButtonPrefab; + + elementRequiredMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); + elementRequiredMods.name = "Required Mods"; + elementRequiredMods.Collapse(true); + + elementExtraMods = CreateModElement($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); + elementExtraMods.name = "Extra Mods"; + elementExtraMods.Collapse(true); + } + + private CollapsibleElement CreateModElement(string label, CollapsibleElement parent = null) + { + // Container for required mods + RectTransform rt = Instantiate(navigationButtonPrefab, detailsContent).GetComponent(); + + CollapsibleElement element = rt.GetComponent(); + CollapsibleElementVisualController controller = rt.GetComponent(); + + controller.categoryTextColor = Color.white; + controller.articleTextColor = Color.white; + controller.collapseIndicatorImage.color = new(1f, 1f, 1f, 0x50 / 255f); + + element.SetText(label); + + if (parent != null) + { + var last = parent.childElements.LastOrDefault() ?? parent; + element.transform.SetSiblingIndex(last.transform.GetSiblingIndex() + 1); + parent.AddChild(element); + + // Remove the Button to allow the hyperlink handler to work + Component.Destroy(element.GetComponentInChildren(true)); + + //// Enable hyperlink parsing + HyperlinkHandler modHyperlinkHandler = controller.elementText.GetOrAddComponent(); + modHyperlinkHandler.linkColor = new UnityEngine.Color(0.302f, 0.651f, 1f); // #4DA6FF + modHyperlinkHandler.linkHoverColor = new UnityEngine.Color(0.498f, 0.749f, 1f); // #7FBFFF + modHyperlinkHandler.ApplyLinkStyling(); + } + + return element; + } + + private void ClearModElements(CollapsibleElement parent) + { + if (parent == null) + return; + + parent.Collapse(true); + + if (parent.childElements.Count == 0) + return; + + foreach (var element in parent.childElements) + GameObject.Destroy(element.gameObject); + + parent.childElements.Clear(); + } + + private void CollapsibleElementClicked(CollapsibleElement element) + { + element.Toggle(); + } + + private void SetupListeners(bool on) + { + if (on) + { + serverGridView.SelectedIndexChanged += this.OnSelectedIndexChanged; + elementRequiredMods.CollapsibleElementClicked += CollapsibleElementClicked; + elementExtraMods.CollapsibleElementClicked += CollapsibleElementClicked; + } + else + { + serverGridView.SelectedIndexChanged -= this.OnSelectedIndexChanged; + elementRequiredMods.CollapsibleElementClicked -= CollapsibleElementClicked; + elementExtraMods.CollapsibleElementClicked -= CollapsibleElementClicked; + } + } + #endregion + + #region UI callbacks + private void RefreshAction() + { + if (serverRefreshing) + return; + + remoteServers.Clear(); + + serverRefreshing = true; + //buttonJoin.ToggleInteractable(false); + buttonRefresh.ToggleInteractable(false); + + if (DVSteamworks.Success) + ListActiveLobbies(); + + } + private void JoinAction() + { + if (selectedServer == null || connectionState != ConnectionState.NotConnected) + return; + + 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; + _ = JoinLobby((Lobby)selectedLobby); + } + else + { + Multiplayer.LogWarning($"JoinAction called but lobby is null"); + AttemptFail(); + } + } + + private void DirectAction() + { + if (connectionState != ConnectionState.NotConnected) + return; + + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); + + //making a direct connection + direct = true; + password = null; + + ShowIpPopup(); + } + + private void OnSelectedIndexChanged(MPGridView gridView) + { + if (serverRefreshing) + return; + + selectedServer = gridView.SelectedItem; + if (selectedServer != null && incompatibleMods == false) + { + UpdateDetailsPane(); + + // Check if we can connect to this server + Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); + Multiplayer.Log($"Client: \"{MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); + + bool canConnect = selectedServer.GameVersion == MainMenuControllerPatch.MenuProvider.BuildVersionString && + selectedServer.MultiplayerVersion == Multiplayer.Ver; + + buttonJoin.ToggleInteractable(canConnect); + } + else + { + buttonJoin.ToggleInteractable(false); + } + } + + private void UpdateElement(IServerBrowserGameDetails element) + { + int index = serverGridView.IndexOf(element); + + if (index >= 0) + { + var viewElement = serverGridView.GetElementAt(index) as ServerBrowserElement; + viewElement?.UpdateView(); + } + } + #endregion + + private void UpdateDetailsPane() + { + StringBuilder details = new(); + + if (selectedServer != null) + { + serverName.text = selectedServer.Name; + + // Note: built-in localisations have a trailing colon e.g. 'Game mode:' + + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
"); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
"); + details.Append(FORMAT_ALPHA + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
"); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
"); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__PASSWORD_REQUIRED_YES : Locale.SERVER_BROWSER__PASSWORD_REQUIRED_NO) + "
"); + details.Append(FORMAT_ALPHA + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString ? "" : "") + selectedServer.GameVersion + "
"); + + details.Append(selectedServer.ServerDetails); + + if (selectedServer.ServerDetails != null && selectedServer.ServerDetails.Length > 0) + details.Append("
"); + + detailsPane.text = details.ToString(); + + // Build mod lists + ClearModElements(elementRequiredMods); + ClearModElements(elementExtraMods); + + var localMods = ModCompatibilityManager.Instance.GetLocalMods(); + + BuildServerMods(selectedServer.RequiredMods, localMods); + BuildLocalMods(localMods); + } + else + { + serverName.text = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; + 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."; + + ClearModElements(elementRequiredMods); + ClearModElements(elementExtraMods); + } + + } + + /// + /// Validates the client has all required mods for the server and the versions match. + /// Populates the mod details list. + /// + /// + /// + /// + /// true if all required mods are present and have correct versions, false if any mods are missing or there is a version mismatch. + private bool BuildServerMods(ModInfo[] serverMods, ModInfo[] localMods) + { + bool modsOk = true; + + if (serverMods == null || localMods == null) + { + Multiplayer.LogWarning("BuildServerMods() called with null serverMods or localMods"); + return false; + } + + if (selectedServer.RequiredMods != null && selectedServer.RequiredMods.Length > 0) + { + Multiplayer.LogDebug(() => $"Parsed {serverMods?.Length} mods from server \"{selectedServer?.Name}\""); + + foreach (var mod in serverMods) + { + ModInfo modMatch = localMods.FirstOrDefault(l => l.Id == mod.Id); + + Multiplayer.LogDebug(() => $"Checking mod \"{mod.Id}\" v\"{mod.Version}\" - Found: \"{modMatch.Id}\" v\"{modMatch.Version}\""); + + bool modFound = modMatch.Id == mod.Id; + bool modVersionMatch = modFound && modMatch.Version == mod.Version; + + modsOk &= modVersionMatch; + + string status; + if (modFound && modVersionMatch) + status = $"{Locale.SERVER_BROWSER__OK}"; + else if (modFound && !modVersionMatch) + status = $"{Locale.SERVER_BROWSER__MISMATCH}"; + else + status = $"{Locale.SERVER_BROWSER__MISSING}"; + + var link = !string.IsNullOrEmpty(mod.Url) ? $"{mod.Id}" : mod.Id; + + var element = CreateModElement(mod.Id, elementRequiredMods); + element.isLeaf = true; + element.SetText($"{link} ({mod.Version}) - {status}"); + } + + elementRequiredMods.Expand(false); + + if (modsOk) + { + elementRequiredMods.Collapse(false); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}: {Locale.SERVER_BROWSER__OK}"); + } + else + { + elementRequiredMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__REQUIRED_MODS}"); + } + } + + return modsOk; + } + + /// + /// Validates the client does not have any mods that the server is not running and does not have any mods incompatible with Multiplayer. + /// Populates the mod details list. + /// + /// + /// + /// + /// true if there are no conflicting mods, false if any mods can not be used with this server. + private bool BuildLocalMods(ModInfo[] localMods) + { + bool modsOk = true; + + if (localMods == null || selectedServer?.RequiredMods == null) + { + Multiplayer.LogWarning($"BuildLocalMods() localMods is null: {localMods == null}, requiredMods is null: {selectedServer?.RequiredMods == null}"); + return false; + } + + var extraMods = localMods.Where(l => !selectedServer.RequiredMods.Any(m => m.Id == l.Id)).ToArray(); + Multiplayer.LogDebug(() => $"Found {extraMods.Length} extra mods on client for server \"{selectedServer.Name}\""); + + if (extraMods.Length > 0) + { + string status; + foreach (var mod in extraMods) + { + var compatibility = ModCompatibilityManager.Instance.GetCompatibility(mod); + if (compatibility == MultiplayerCompatibility.Incompatible) + { + status = $"{Locale.SERVER_BROWSER__INCOMPATIBLE}"; + modsOk = false; + } + else if (compatibility == MultiplayerCompatibility.Undefined || compatibility == MultiplayerCompatibility.All) + { + status = $"{Locale.SERVER_BROWSER__EXTRA_MOD}"; + modsOk = false; + } + else + { + status = $"{Locale.SERVER_BROWSER__OK}"; + } + + var element = CreateModElement(mod.Id, elementExtraMods); + element.isLeaf = true; + element.SetText($"{mod.Id} ({mod.Version}) - {status}"); + } + + elementExtraMods.Expand(false); + + if (modsOk) + { + elementExtraMods.Collapse(false); + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}: {Locale.SERVER_BROWSER__OK}"); + } + else + { + elementExtraMods.SetText($"{FORMAT_ALPHA}{Locale.SERVER_BROWSER__EXTRA_MODS}"); + } + } + + return modsOk; + } + + 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); + OnSelectedIndexChanged(serverGridView); //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 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) + { + AttemptFail(); + return; + } + + password = result.data; + + if (direct) + { + //store params for later + Multiplayer.Settings.LastRemoteIP = address; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; + } + + InitiateConnection(); + }; + } + + 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 void InitiateConnection() + { + + Multiplayer.Log($"Initiating connection. Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); + + attempt = 0; + ShowConnectingPopup(); + + if (!direct && joinedLobby != null) + { + connectionState = ConnectionState.AttemptingSteamRelay; + string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); + NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); + 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(); + } + } + else + { + 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 + } + + joinedLobby?.Leave(); + joinedLobby = null; + + if (gameObject != null && gameObject.activeInHierarchy) + { + if (serverGridView != null) + OnSelectedIndexChanged(serverGridView); + + if (buttonDirectIP != null && buttonDirectIP.gameObject != null) + buttonDirectIP.ToggleInteractable(true); + } + + StartCoroutine(ResetConnectionState()); + } + + private IEnumerator ResetConnectionState() + { + yield return new WaitForSeconds(1.0f); + connectionState = ConnectionState.NotConnected; + } + + private void OnDisconnect(DisconnectReason reason, string message) + { + Multiplayer.Log($"Disconnected due to: {reason}, \"{message}\""); + + string displayMessage = !string.IsNullOrEmpty(message) + ? message + : GetDisplayMessageForDisconnect(reason); + + Multiplayer.LogDebug(() => "OnDisconnect() Leaving Lobby"); + joinedLobby?.Leave(); + joinedLobby = null; + + connectionState = ConnectionState.NotConnected; + AttemptFail(); + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + Multiplayer.LogDebug(() => "OnDisconnect() Queuing"); + MainMenuThingsAndStuff.Instance?.ShowOkPopup(displayMessage, () => { }); + }); + } + + private string GetDisplayMessageForDisconnect(DisconnectReason reason) + { + return reason switch + { + DisconnectReason.UnknownHost => Locale.DISCONN_REASON__UNKNOWN_HOST, //"Unknown Host", + DisconnectReason.DisconnectPeerCalled => Locale.DISCONN_REASON__PLAYER_KICKED, //"Player Kicked", + DisconnectReason.ConnectionFailed => Locale.DISCONN_REASON__HOST_UNREACHABLE, //"Host Unreachable", + DisconnectReason.ConnectionRejected => Locale.DISCONN_REASON__REJECTED, //"Rejected!", + DisconnectReason.RemoteConnectionClose => Locale.DISCONN_REASON__SHUTTING_DOWN, //"Server Shutting Down", + DisconnectReason.Timeout => Locale.DISCONN_REASON__HOST_TIMED_OUT, //"Server Timed Out", + _ => "Connection Failed" + }; + } + #endregion + + + #region steam lobby + private async void ListActiveLobbies() + { + lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100) + .FilterDistanceWorldwide() + .WithSlotsAvailable(-1) + //.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 serverGridView.Items) + { + 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() + { + // Get all active IDs + List activeIDs = remoteServers.Select(s => s.id).Distinct().ToList(); + + // Remove servers that no longer exist + for (int i = serverGridView.Items.Count - 1; i >= 0; i--) + { + if (!activeIDs.Contains(serverGridView.Items[i].id)) + { + serverGridView.RemoveItemAt(i); + } + } + + Multiplayer.LogDebug(() => $"RefreshGridView() prepare to update/add, remoteServers count: {remoteServers.Count}"); + // Update existing servers and add new ones + foreach (var server in remoteServers) + { + var existingServer = serverGridView.Items.FirstOrDefault(gv => gv.id == server.id); + if (existingServer != null) + { + Multiplayer.LogDebug(() => $"RefreshGridView() updating server"); + // Update existing server + existingServer.TimePassed = server.TimePassed; + existingServer.CurrentPlayers = server.CurrentPlayers; + existingServer.LocalIPv4 = server.LocalIPv4; + existingServer.LastSeen = server.LastSeen; + } + else + { + Multiplayer.LogDebug(() => $"RefreshGridView() adding server"); + // Add new server + serverGridView.AddItem(server); + } + } + } + + 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; + } + + private async Task JoinLobby(Lobby lobby) + { + + if (connectionState != ConnectionState.NotConnected) + { + Multiplayer.LogWarning($"Cannot join lobby while in state: {connectionState}"); + return false; + } + + connectionState = ConnectionState.JoiningLobby; + Multiplayer.Log($"Attempting to join lobby ({lobby.Id})"); + + var joinResult = await lobby.Join(); + + if (joinResult == RoomEnter.Success) + { + Multiplayer.Log($"Lobby joined ({lobby.Id})"); + + joinedLobby = lobby; + lobbyToJoin = null; + + string hasPass = lobby.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); + Multiplayer.Log($"Lobby ({lobby.Id}) has password: {hasPass}"); + + if (string.IsNullOrEmpty(hasPass) || hasPass == "False") + { + Multiplayer.Log($"Attempting connection..."); + InitiateConnection(); + } + else + { + connectionState = ConnectionState.AwaitingPassword; + Multiplayer.Log($"Prompting for password..."); + ShowPasswordPopup(); + } + + return true; + } + else + { + Multiplayer.LogDebug(() => "JoinLobby() Leaving Lobby"); + lobby.Leave(); + joinedLobby = null; + Multiplayer.Log($"Failed to join lobby: {joinResult}"); + AttemptFail(); + } + + return false; + } +} diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs new file mode 100644 index 00000000..4ee5ecd5 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -0,0 +1,331 @@ +using DV.Logic.Job; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedJob : IdMonoBehaviour +{ + #region Lookup Cache + + private static readonly Dictionary jobToNetworkedJob = []; + private static readonly Dictionary jobIdToNetworkedJob = []; + private static readonly Dictionary jobIdToJob = []; + + public static bool Get(ushort netId, out NetworkedJob obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedJob)rawObj; + return b; + } + + public static bool TryGetJob(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); + } + + public static bool TryGetNetId(Job job, out ushort netId) + { + if (TryGetFromJob(job, out var networkedJob)) + { + netId = networkedJob.NetId; + return true; + } + + netId = 0; + return false; + } + + #endregion + + private static readonly Dictionary> pendingJobTasks = []; + + 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 readonly List JobReports = []; + + 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 = []; + + private bool tasksInitialized = false; + + protected override void Awake() + { + base.Awake(); + } + + protected 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; + + transform.SetParent(station.transform); + + // 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(); + } + + // Check for any pending tasks that were added before the NetworkedJob was created + if (pendingJobTasks.TryGetValue(job, out var taskList) && taskList != null) + { + pendingJobTasks.Remove(job); + + //Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Found {taskList.Count} pending tasks for jobId {job.ID}"); + + foreach (var task in taskList) + CreateNetworkedTask(task); + } + + tasksInitialized = true; + + //Multiplayer.LogDebug(() => $"NetworkedJob.Initialize(): Initialized NetworkedJob for jobId {job.ID} with {Job.tasks.Count} tasks"); + } + + private void AddToCache() + { + jobToNetworkedJob[Job] = this; + jobIdToNetworkedJob[Job.ID] = this; + jobIdToJob[Job.ID] = Job; + //Multiplayer.Log($"NetworkedJob added to cache: {Job.ID}"); + } + + public static void EnqueueTask(Task task, Job job) + { + if (TryGetFromJob(job, out var netJob) || netJob != null && netJob.tasksInitialized) + { + Multiplayer.LogDebug(() => $"NetworkedJob.EnqueueTask(): Creating task immediately for jobId {task.Job.ID}"); + netJob.CreateNetworkedTask(task); + return; + } + + Multiplayer.LogDebug(() => $"NetworkedJob.EnqueueTask(): Enqueuing task for later creation for jobId {task.Job.ID}"); + + if (!pendingJobTasks.TryGetValue(task.Job, out var taskList)) + { + taskList = []; + pendingJobTasks[task.Job] = taskList; + } + taskList.Add(task); + } + + public void SetTasksFromServer(Dictionary netIdToTask) + { + if (netIdToTask == null) + { + Multiplayer.LogError($"NetworkedJob.SetTasksFromServer(): netIdToTask is null for jobId {Job?.ID}"); + return; + } + + foreach (var kvp in netIdToTask) + { + CreateNetworkedTask(kvp.Value, kvp.Key); + } + } + + private void CreateNetworkedTask(Task task, ushort netId = 0) + { + if (task == null) + { + Multiplayer.LogError($"NetworkedJob.CreateNetworkedTask(): Task is null for jobId {Job?.ID}"); + return; + } + + NetworkedTask taskObj = new GameObject().AddComponent(); + taskObj.Initialize(task, netId); + taskObj.name = $"{Job.ID}-{taskObj.NetId}"; + taskObj.transform.SetParent(transform); + } + + 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(); + } + + protected 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/Jobs/NetworkedTask.cs b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs new file mode 100644 index 00000000..a938b837 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedTask.cs @@ -0,0 +1,97 @@ +using DV.Logic.Job; +using System; +using System.Collections.Generic; + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedTask : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary taskToNetworkedTask = []; + + public static bool TryGet(ushort netId, out NetworkedTask networkedTask) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + networkedTask = (NetworkedTask)rawObj; + return b; + } + + public static bool TryGet(Task task, out NetworkedTask networkedTask) + { + return taskToNetworkedTask.TryGetValue(task, out networkedTask); + } + + public static bool TryGet(ushort netId, out Task task) + { + task = null; + + if (!Get(netId, out IdMonoBehaviour rawObj) || rawObj == null) + return false; + + task = ((NetworkedTask)rawObj).Task; + + return task != null; + } + + public static bool TryGetNetId(Task task, out ushort netId) + { + if (taskToNetworkedTask.TryGetValue(task, out var networkedTask) && networkedTask != null) + { + netId = networkedTask.NetId; + return true; + } + + netId = 0; + return false; + } + #endregion + + protected override bool IsIdServerAuthoritative => true; + + public Task Task { get; private set; } + + private float lastStartTime; + private float lastFinishTime; + private TaskState lastState; + + public void Initialize(Task task, ushort netId = 0) + { + if (task == null) + { + Multiplayer.LogError($"NetworkedTask.Initialize(): Task is null\r\n{Environment.StackTrace}"); + return; + } + + if (taskToNetworkedTask.ContainsKey(task)) + { + Multiplayer.LogError($"NetworkedTask.Initialize(): Task {task.InstanceTaskType} for jobId {task.Job.ID} is already registered"); + Destroy(this); + return; + } + + Task = task; + taskToNetworkedTask[Task] = this; + if (netId != 0) + NetId = netId; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + if (Task != null) + taskToNetworkedTask.Remove(Task); + } + + public void SetState(TaskState newState) + { + if (lastState == newState && lastStartTime == Task.taskStartTime && lastFinishTime == Task.taskFinishTime) + return; + + lastState = newState; + lastStartTime = Task.taskStartTime; + lastFinishTime = Task.taskFinishTime; + + NetworkLifecycle.Instance.Server.SendTaskUpdate(NetId, newState, Task.taskStartTime, Task.taskFinishTime); + } +} diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs new file mode 100644 index 00000000..bb89aecf --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedWarehouseMachineController.cs @@ -0,0 +1,220 @@ +using DV.Logic.Job; +using DV.ThingTypes; +using DV.ThingTypes.TransitionHelpers; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Clientbound.Jobs; +using System.Collections.Generic; +using static WarehouseMachineController; + + + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedWarehouseMachineController : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary warehouseMachineControllerToNetworked = []; + private static readonly Dictionary warehouseMachineToNetworked = []; + + public static bool Get(ushort netId, out NetworkedWarehouseMachineController obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedWarehouseMachineController)rawObj; + return b; + } + + public static bool TryGetNetId(WarehouseMachineController warehouseMachineController, out ushort netId) + { + if (GetFromWarehouseMachineController(warehouseMachineController, out var networkedWarehouseMachineController)) + { + netId = networkedWarehouseMachineController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGetNetId(WarehouseMachine warehouseMachine, out ushort netId) + { + var networkedWarehouseMachineController = GetFromWarehouseMachine(warehouseMachine); + if (networkedWarehouseMachineController != null) + { + netId = networkedWarehouseMachineController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGet(ushort netId, out WarehouseMachineController warehouseMachineController) + { + if (Get(netId, out var networkedWarehouseMachineController)) + { + warehouseMachineController = networkedWarehouseMachineController.WarehouseMachineController; + return true; + } + + warehouseMachineController = null; + return false; + } + + public static bool TryGet(ushort netId, out WarehouseMachine warehouseMachine) + { + if (Get(netId, out var networkedWarehouseMachineController)) + { + warehouseMachine = networkedWarehouseMachineController.WarehouseMachine; + return true; + } + + warehouseMachine = null; + return false; + } + + public static bool GetFromWarehouseMachineController(WarehouseMachineController warehouseMachineController, out NetworkedWarehouseMachineController networkedWarehouseMachineController) + { + return warehouseMachineControllerToNetworked.TryGetValue(warehouseMachineController, out networkedWarehouseMachineController); + } + + public static NetworkedWarehouseMachineController GetFromWarehouseMachine(WarehouseMachine warehouseMachine) + { + //fast path lookup + if (warehouseMachineToNetworked.TryGetValue(warehouseMachine, out NetworkedWarehouseMachineController networkedWarehouseMachineController)) + return networkedWarehouseMachineController; + + //cache miss, try to find parent WarehouseMachineController + var warehouseMachineController = GetFomId(warehouseMachine.ID); + if (warehouseMachineController != null) + { + //Warehouse Machine Controller found, check for NetworkedWarehouseMachineController + if (!GetFromWarehouseMachineController(warehouseMachineController, out networkedWarehouseMachineController) && networkedWarehouseMachineController != null) + warehouseMachineToNetworked[warehouseMachine] = networkedWarehouseMachineController; + } + + return networkedWarehouseMachineController; + } + + private static WarehouseMachineController GetFomId(string ID) + { + foreach (var warehouse in WarehouseMachineController.allControllers) + { + if (warehouse.warehouseMachine.ID == ID) + { + return warehouse; + } + } + return null; + } + + #endregion + protected override bool IsIdServerAuthoritative => false; + + public string Id => WarehouseMachine?.ID; + public WarehouseMachineController WarehouseMachineController { get; private set; } + public WarehouseMachine WarehouseMachine => WarehouseMachineController?.warehouseMachine; + + protected override void Awake() + { + base.Awake(); + WarehouseMachineController = GetComponent(); + warehouseMachineControllerToNetworked[WarehouseMachineController] = this; + } + + protected override void OnDestroy() + { + base.OnDestroy(); + + warehouseMachineControllerToNetworked.Remove(WarehouseMachineController); + + if (WarehouseMachineController.warehouseMachine != null) + warehouseMachineToNetworked.Remove(WarehouseMachineController.warehouseMachine); + } + + public void ServerProcessWarehouseAction(WarehouseAction action) + { + Multiplayer.LogDebug(() => $"ServerProcessWarehouseAction() {Id}, Action Type: {action}"); + switch (action) + { + case WarehouseAction.Load: + WarehouseMachineController.StartLoadSequence(); + break; + + case WarehouseAction.Unload: + WarehouseMachineController.StartUnloadSequence(); + break; + } + } + + public void ClientProcessUpdate(ClientboundWarehouseControllerUpdatePacket packet) + { + TextPreset preset = (TextPreset)packet.Preset; + bool isLoading = packet.IsLoading; + string jobId = null; + Car car = null; + CargoType_v2 cargoType_V2 = null; + string extra = null; + + if (WarehouseMachineController == null) + return; + + if (packet.CarNetId != 0) + { + if (!NetworkedTrainCar.TryGet(packet.CarNetId, out NetworkedTrainCar networkedCar)) + { + Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find TrainCar with NetId: {packet.NetId}"); + return; + } + + car = networkedCar.TrainCar.logicCar; + } + + if (packet.JobNetId != 0) + { + if (!NetworkedJob.Get(packet.JobNetId, out var networkedJob)) + { + Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find Job with NetId: {packet.JobNetId}"); + return; + } + + jobId = networkedJob.Job.ID; + } + + if (car != null && jobId != null) + { + if (!CargoTypeLookup.Instance.TryGet(packet.CargoTypeNetId, out cargoType_V2)) + { + Multiplayer.LogWarning($"NetworkedWarehouseMachineController failed to find CargoType with netId: {packet.CargoTypeNetId} for JobId: {jobId} on Car: {car.ID}"); + } + } + + WarehouseMachineController?.SetScreen(preset, isLoading, jobId, car, cargoType_V2, extra); + + //special case for car updated - remove task from machine + if (preset == TextPreset.CarUpdated && WarehouseMachine != null) + { + CleanupTask(isLoading, car); + } + + //special case for clearing - play sound + if (preset == TextPreset.ClearDesc) + WarehouseMachineController?.machineSound?.Play(WarehouseMachineController.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, base.transform, false, 0f, null); + + } + + private void CleanupTask(bool isLoading, Car car) + { + List currentLoadUnloadData = WarehouseMachine.GetCurrentLoadUnloadData(isLoading ? WarehouseTaskType.Loading : WarehouseTaskType.Unloading); + + foreach (var data in currentLoadUnloadData) + { + if (data.tasksAvailableToProcess == null) + continue; + + foreach (var task in data.tasksAvailableToProcess) + WarehouseMachine.RemoveWarehouseTask(task); + + } + } +} diff --git a/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs b/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs new file mode 100644 index 00000000..afeb48f8 --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/WarehouseMachineLookup.cs @@ -0,0 +1,91 @@ +using DV.Logic.Job; +using DV.Utils; +using JetBrains.Annotations; +using System.Collections.Generic; + +namespace Multiplayer.Components.Networking.Jobs; + +public class WarehouseMachineLookup : SingletonBehaviour +{ + private static readonly Dictionary netIdToWarehouseMachine = []; + + public void RegisterWarehouseMachine(WarehouseMachine machine) + { + Multiplayer.LogDebug(() => $"RegisterWarehouseMachine() {machine.WarehouseTrack.ID}, machineID: {machine.ID}"); + + if (machine == null) + return; + + if (string.IsNullOrEmpty(machine.ID)) + { + Multiplayer.LogDebug(() => $"Attempted to register WarehouseMachine with null or empty ID for track {machine.WarehouseTrack.ID}"); + return; + } + + ushort netId = GenerateNetId(machine.ID); + + if (netIdToWarehouseMachine.ContainsKey(netId)) + { + var existing = netIdToWarehouseMachine[netId]; + Multiplayer.LogWarning(() => $"Registering WarehouseMachine for track {machine.WarehouseTrack.ID}, machineID: {machine.ID} failed! More than one WarehouseMachine with the same ID!"); + return; + } + + Multiplayer.LogDebug(() => $"Registered WarehouseMachine for track {machine.WarehouseTrack.ID}, machineID: {machine.ID}, netId: {netId}"); + netIdToWarehouseMachine[netId] = machine; + } + + public static bool TryGet(ushort netId, out WarehouseMachine machine) + { + var result = netIdToWarehouseMachine.TryGetValue(netId, out machine); + + if (result && machine == null) + { + netIdToWarehouseMachine.Remove(netId); + return false; + } + + return result; + } + + public static bool TryGetNetId(WarehouseMachine machine, out ushort netId) + { + //Multiplayer.LogDebug(() => $"Trying to get NetID for WarehouseMachine on track {machine?.WarehouseTrack?.ID}, machineID: {machine?.ID}"); + + if (machine != null && !string.IsNullOrEmpty(machine.ID)) + { + netId = GenerateNetId(machine.ID); + var temp = netId; + //Multiplayer.LogDebug(() => $"Trying to get NetID for WarehouseMachine on track {machine?.WarehouseTrack?.ID}, machineID: {machine?.ID}, netId: {temp}"); + + if (netIdToWarehouseMachine.ContainsKey(netId)) + return true; + } + + netId = 0; + return false; + } + + private static ushort GenerateNetId(string id) + { + unchecked + { + int hash = id.GetHashCode(); + ushort result = (ushort)((hash & 0xFFFF) ^ ((hash >> 16) & 0xFFFF)); + return result == 0 ? (ushort)1 : result; + } + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(WarehouseMachineLookup)}]"; + } + + protected override void Awake() + { + base.Awake(); + netIdToWarehouseMachine.Clear(); + Multiplayer.LogDebug(() => $"{nameof(WarehouseMachineLookup)} Awake, cleared existing lookup dictionary."); + } +} diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 7c14288d..8c866e81 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -1,15 +1,20 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Text; using DV.Scenarios.Common; using DV.Utils; using LiteNetLib; -using LiteNetLib.Utils; -using Multiplayer.Networking.Listeners; +using MPAPI; +using Multiplayer.API; +using Multiplayer.Components.Networking.UI; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Client; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.Managers; using Multiplayer.Utils; -using UnityEngine; +using System.Collections.Generic; +using System.Collections; +using System.Net; +using System; using UnityEngine.SceneManagement; +using UnityEngine; namespace Multiplayer.Components.Networking; @@ -19,6 +24,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 +38,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 +46,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(ServerPlayer player) { - return Server?.IsRunning == true && Client?.IsRunning == true && Client?.selfPeer?.Id == peer?.Id; + return Server?.IsRunning == true && Client?.IsRunning == true && Client.PlayerId == player.PlayerId; } /// @@ -50,7 +60,7 @@ public bool IsHost(NetPeer peer) /// public bool IsHost() { - return IsHost(Client?.selfPeer); + return Server?.IsRunning == true; } private readonly Queue mainMenuLoadedQueue = new(); @@ -60,30 +70,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,26 +111,55 @@ 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); + if (!server.Start(port)) return false; + Server = server; - StartClient("localhost", port, Multiplayer.Settings.Password); + + // Register server API + var serverAPI = new ServerAPIProvider(server); + MultiplayerAPI.RegisterServer(serverAPI); + + StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null); + + //reset for next game + IsSinglePlayer = true; + serverData = null; + 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); + NetworkClient client = new(Multiplayer.Settings, isSinglePlayer); + client.Start(address, port, password, isSinglePlayer, onDisconnect); + Client = client; + + // Register server API + var clientAPI = new ClientAPIProvider(client); + MultiplayerAPI.RegisterClient(clientAPI); + OnSettingsUpdated(Multiplayer.Settings); // Show stats if enabled } @@ -156,8 +185,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); @@ -169,7 +201,9 @@ private void TickManager(NetworkManager manager) { if (manager == null) return; + tickWatchdog.Start(); + try { manager.PollEvents(); @@ -186,14 +220,24 @@ private void TickManager(NetworkManager manager) public void Stop() { - if (Stats != null) Stats.Hide(); - Server?.Stop(); - Client?.Stop(); - Server = null; - Client = null; + Stats?.Hide(); + + if (Server != null) + { + Server?.Stop(); + MultiplayerAPI.ClearServer(); + Server = null; + } + + if (Client != null) + { + Client?.Stop(); + MultiplayerAPI.ClearClient(); + Client = null; + } } - private void OnApplicationQuit() + protected void OnApplicationQuit() { Stop(); } @@ -206,4 +250,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 fec0ea65..5be853fd 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -1,16 +1,40 @@ -using System; +using DV.Player; using Multiplayer.Components.Networking.Train; using Multiplayer.Editor.Components.Player; using UnityEngine; namespace Multiplayer.Components.Networking.Player; +/// +/// Represents a networked player in the multiplayer environment, handling movement, item holding, and visual state +/// public class NetworkedPlayer : MonoBehaviour { + #region Static Setup + private static Vector3 itemAnchorOffset = new(0.2f, 1.5f, 0.4f); + + /// + /// Captures the standard offset position for held items relative to the player transform + /// for mapping to a NetworkedPlayer + /// This must be called as soon as the world is loaded, before the local player moves or crouches + /// + public static void CaptureItemAnchorOffset() + { + //todo: there's some minor inconsistency with return values and may be related to: + // - the direction/rotation of the camera + // - player loading status (maybe posistion hasn't settled yet) + if (!VRManager.IsVREnabled()) + { + itemAnchorOffset = PlayerManager.PlayerTransform.InverseTransformPoint(ItemPositionController.Instance.itemAnchor.position); + Multiplayer.LogDebug(() => $"NetworkedPlayer.CaptureItemAnchorOffset() itemAnchorOffset: {itemAnchorOffset}"); + } + } + + #endregion + private const float LERP_SPEED = 5.0f; - public byte Id; - public Guid Guid; + public byte PlayerId { get; set; } private AnimationHandler animationHandler; private NameTag nameTag; @@ -18,23 +42,30 @@ public class NetworkedPlayer : MonoBehaviour private string username; - public string Username { + public string Username + { get => username; - set { + set + { username = value; nameTag.SetUsername(value); } } - private bool isOnCar; + internal bool IsOnCar { get; private set; } + internal TrainCar OccupiedCar { get; private set; } private Transform selfTransform; private Vector3 targetPos; private Quaternion targetRotation; private Vector2 moveDir; private Vector2 targetMoveDir; + + private GameObject itemHeld; + private Vector3? itemHoldPos; + private Quaternion? itemHoldRot; - private void Awake() + protected void Awake() { animationHandler = GetComponent(); @@ -52,6 +83,11 @@ private void Awake() targetMoveDir = Vector2.zero; } + protected void OnDestroy() + { + Settings.OnSettingsUpdated -= OnSettingsUpdated; + } + private void OnSettingsUpdated(Settings settings) { nameTag.ShowUsername(settings.ShowNameTags); @@ -69,45 +105,108 @@ public int GetPing() return ping; } - private void Update() + protected void Update() { float t = Time.deltaTime * LERP_SPEED; - Vector3 position = Vector3.Lerp(isOnCar ? selfTransform.localPosition : selfTransform.position, isOnCar ? targetPos : targetPos + WorldMover.currentMove, t); - Quaternion rotation = Quaternion.Lerp(isOnCar ? selfTransform.localRotation : selfTransform.rotation, targetRotation, t); - + Vector3 position = Vector3.Lerp(IsOnCar ? selfTransform.localPosition : selfTransform.position, IsOnCar ? targetPos : targetPos + WorldMover.currentMove, t); + moveDir = Vector2.Lerp(moveDir, targetMoveDir, t); animationHandler.SetMoveDir(moveDir); - if (isOnCar) + if (IsOnCar && OccupiedCar != null) { selfTransform.localPosition = position; - selfTransform.localRotation = rotation; + + // Calculate a world-up-respecting rotation + // This creates a rotation where Y points up in world space + // but the forward direction aligns with the car's forward projected onto the horizontal plane + Vector3 carForward = OccupiedCar.transform.forward; + Vector3 worldUp = Vector3.up; + + // Project car's forward onto the horizontal plane + Vector3 horizontalForward = Vector3.ProjectOnPlane(carForward, worldUp).normalized; + if (horizontalForward.sqrMagnitude < 0.001f) + horizontalForward = Vector3.ProjectOnPlane(OccupiedCar.transform.right, worldUp).normalized; + + // Create base orientation aligned with world up but facing car's forward direction + Quaternion baseRotation = Quaternion.LookRotation(horizontalForward, worldUp); + + // Calculate the relative rotation: how much is the player rotated relative to the car? + float carYaw = baseRotation.eulerAngles.y; + float playerYaw = targetRotation.eulerAngles.y; + float relativeYaw = playerYaw - carYaw; + + // Apply the desired Y rotation (player's facing direction) on top of this base rotation + Quaternion targetWorldRotation = baseRotation * Quaternion.Euler(0, relativeYaw, 0); + + // Apply rotation in world space despite being a child transform + selfTransform.rotation = Quaternion.Lerp(selfTransform.rotation, targetWorldRotation, t); } else { selfTransform.position = position; - selfTransform.rotation = rotation; + selfTransform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, t); + } + + if (itemHeld != null) + { + itemHeld.transform.position = selfTransform.position + GetItemOffsetFromPlayer(); + itemHeld.transform.rotation = selfTransform.rotation * (itemHoldRot ?? Quaternion.identity);//ItemPositionController.Instance.itemAnchor.localRotation); } } - public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotation, bool isJumping, bool movePacketIsOnCar) + public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotationY, bool isJumping, bool movePacketIsOnCar) { + targetPos = position; targetMoveDir = moveDir; + animationHandler.SetIsJumping(isJumping); - if (isOnCar != movePacketIsOnCar) + if (IsOnCar != movePacketIsOnCar) return; - targetPos = position; - targetRotation = Quaternion.Euler(0, rotation, 0); + targetRotation = Quaternion.Euler(0, rotationY, 0); } public void UpdateCar(ushort netId) { - isOnCar = NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar); - selfTransform.SetParent(isOnCar ? trainCar.transform : null, true); - targetPos = isOnCar ? transform.localPosition : selfTransform.position; - targetRotation = isOnCar ? transform.localRotation : selfTransform.rotation; + IsOnCar = NetworkedTrainCar.TryGet(netId, out TrainCar trainCar); + OccupiedCar = trainCar; + + if (IsOnCar) + selfTransform.SetParent(OccupiedCar.transform, true); + else + selfTransform.SetParent(null, true); + } + + /// + /// Sets the player's currently held item with optional position and rotation offsets + /// + /// The item GameObject to hold + /// Optional local position offset + /// Optional local rotation offset + public void HoldItem(GameObject itemGo, Vector3? targetPos = null, Quaternion? targetRot = null) + { + Multiplayer.LogDebug(() => $"NetworkedPlayer.HoldItem({itemGo.GetPath()}) Player: {username}, Before position: {itemGo.transform.localPosition}, rotation: {itemGo.transform.localRotation}, Target pos: {targetPos}, Target rot: {targetRot}"); + + itemHeld = itemGo; + itemHoldPos = targetPos; + itemHoldRot = targetRot; + } + + public void DropItem() + { + itemHeld = null; + itemHoldPos = null; + itemHoldRot = null; } + + private Vector3 GetItemOffsetFromPlayer() + { + Vector3 baseOffset = itemAnchorOffset; + Vector3 finalOffset = itemHoldPos.HasValue ? baseOffset + itemHoldPos.Value : baseOffset; + return selfTransform.TransformDirection(finalOffset); + } + } diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index ccc044b4..c731a93a 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) - OnPlayerConnected(networkedPlayer.Id, networkedPlayer); - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerConnected += OnPlayerConnected; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerDisconnected += OnPlayerDisconnected; + textPrefab = markersController.GetComponentInChildren().gameObject; + foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) + OnPlayerConnected(networkedPlayer); + 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) + private void OnPlayerConnected(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; @@ -61,42 +62,60 @@ private void OnPlayerConnected(byte id, NetworkedPlayer player) text.fontSizeMax = text.fontSize; text.enableAutoSizing = true; - playerIndicators[id] = refs; + playerIndicators[player] = refs; } - private void OnPlayerDisconnected(byte id, NetworkedPlayer player) + private void OnPlayerDisconnected(NetworkedPlayer player) { - if (!playerIndicators.TryGetValue(id, out WorldMapIndicatorRefs refs)) + if (!playerIndicators.TryGetValue(player, out WorldMapIndicatorRefs refs)) return; Destroy(refs.gameObject); - playerIndicators.Remove(id); + playerIndicators.Remove(player); } private void OnTick(uint obj) { - if (!worldMap.initialized) + if (markersController == null || UnloadWatcher.isUnloading) return; UpdatePlayers(); } public void UpdatePlayers() { - foreach (KeyValuePair kvp in playerIndicators) + 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.PlayerId, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); - OnPlayerDisconnected(kvp.Key, null); + OnPlayerDisconnected(kvp.Key); + 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 31447e1b..ab6a492c 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -1,3 +1,4 @@ +using Multiplayer.Components.Networking.Train; using System.Collections.Generic; using UnityEngine; @@ -5,8 +6,14 @@ namespace Multiplayer.Components.Networking; public abstract class TickedQueue : MonoBehaviour { + private const float WARNING_THRESHOLD_SECONDS = 3.0f; + private const uint QUEUE_LENGTH_WARNING = (uint)(NetworkLifecycle.TICK_RATE * WARNING_THRESHOLD_SECONDS); + private const uint SNAPSHOT_GAP_WARNING = (uint)(NetworkLifecycle.TICK_RATE * WARNING_THRESHOLD_SECONDS); + private uint lastTick; + private uint lastReceivedTick; private readonly Queue<(uint, T)> snapshots = new(); + protected string identifier; protected virtual void OnEnable() { @@ -20,19 +27,28 @@ protected virtual void OnDisable() NetworkLifecycle.Instance.OnTick -= OnTick; lastTick = 0; snapshots.Clear(); + identifier = string.Empty; } public void ReceiveSnapshot(T snapshot, uint tick) { if (tick <= lastTick) return; + + if (snapshots.Count >= QUEUE_LENGTH_WARNING) + Multiplayer.LogWarning($"[{GetID()}] Snapshot queue exceeds {QUEUE_LENGTH_WARNING} items. Current size: {snapshots.Count}"); + + if (lastReceivedTick > 0 && tick - lastReceivedTick > SNAPSHOT_GAP_WARNING) + Multiplayer.LogWarning($"[{GetID()}] Large gap between snapshots: {tick - lastReceivedTick} ticks."); + + lastReceivedTick = tick; lastTick = tick; snapshots.Enqueue((tick, snapshot)); } private void OnTick(uint tick) { - if (snapshots.Count == 0) + if (snapshots.Count == 0 || UnloadWatcher.isUnloading) return; while (snapshots.Count > 0) { @@ -41,5 +57,31 @@ private void OnTick(uint tick) } } + public void Clear() + { + snapshots.Clear(); + } + protected abstract void Process(T snapshot, uint snapshotTick); + + private string GetID() + { + if (!string .IsNullOrEmpty(identifier)) + return identifier; + + if (this.gameObject == null) + return "Bad GO"; + + TrainCar car = TrainCar.Resolve(this.gameObject); + int bogie = 0; + + if (car != null) + if (this is NetworkedBogie netBogie) + bogie = (car.Bogies[0] == netBogie.Bogie) ? 1 : 2; + + if (car?.logicCar != null) + identifier = $"{car?.ID ?? gameObject.GetPath()}{(bogie > 0 ? $" Bogie {bogie}" : "")}"; + + return identifier ?? "Unknown"; + } } diff --git a/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs new file mode 100644 index 00000000..36bddd19 --- /dev/null +++ b/Multiplayer/Components/Networking/Train/CargoTypeLookup.cs @@ -0,0 +1,116 @@ +using DV; +using DV.ThingTypes; +using DV.ThingTypes.TransitionHelpers; +using DV.Utils; +using JetBrains.Annotations; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; + +namespace Multiplayer.Components.Networking.Train; + +public class CargoTypeLookup : SingletonBehaviour +{ + private readonly Dictionary hashToCargoTypeV2 = []; + private readonly Dictionary cargoTypeV2ToHash = []; + + protected override void Awake() + { + base.Awake(); + + hashToCargoTypeV2.Clear(); + cargoTypeV2ToHash.Clear(); + + RebuildCache(); + } + + protected void RebuildCache() + { + var missingCargoTypes = Globals.G.Types.cargos.Where(c => !cargoTypeV2ToHash.ContainsKey(c)); + + if (!missingCargoTypes.Any()) + return; + + Multiplayer.LogDebug(() => $"CargoTypeLookup: Found {missingCargoTypes.Count()} missing cargo types, registering..."); + + foreach (var cargoType in missingCargoTypes) + TryGetNetId(cargoType, out _); + } + + public bool TryGet(uint netId, out CargoType_v2 cargoType) + { + if (hashToCargoTypeV2.TryGetValue(netId, out cargoType)) + return true; + + RebuildCache(); + + if (hashToCargoTypeV2.TryGetValue(netId, out cargoType)) + return true; + + Multiplayer.LogWarning($"CargoTypeLookup: Could not find CargoType for netId {netId}"); + + cargoType = CargoType.None.ToV2(); + return false; + } + + public bool TryGet(uint netId, out CargoType cargoType) + { + if (TryGet(netId, out CargoType_v2 cargoTypeV2)) + { + cargoType = cargoTypeV2.v1; + return true; + } + + cargoType = CargoType.None; + return false; + } + + public bool TryGetNetId(CargoType_v2 cargoType, out uint netId) + { + netId = 0; + if (cargoType == null) + return false; + + if (cargoTypeV2ToHash.TryGetValue(cargoType, out netId)) + return true; + + uint hash = StringHashing.Fnv1aHash(cargoType.id); + Multiplayer.LogDebug(()=> $"Registering cargo type '{cargoType.id}', netId: {hash}"); + + if (hash == 0 || hash == uint.MaxValue) + { + Multiplayer.LogError($"Computed hash for cargo type '{cargoType.id}' is {hash}, which is reserved."); + netId = 0; + return false; + } + + if (hashToCargoTypeV2.TryGetValue(hash, out var existingCargoType)) + { + if (existingCargoType.id != cargoType.id) + { + Multiplayer.LogError($"Hash collision detected! Cargo type '{cargoType.id}' has same hash as '{existingCargoType.id}': {hash}."); + netId = 0; + return false; + } + } + + cargoTypeV2ToHash[cargoType] = hash; + hashToCargoTypeV2[hash] = cargoType; + netId = hash; + + //Multiplayer.Log($"CargoType '{cargoType.id}' registered with netId: {netId}"); + + return true; + } + + public bool TryGetNetId(CargoType cargoType, out uint netId) + { + return TryGetNetId(cargoType.ToV2(), out netId); + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(CargoTypeLookup)}]"; + } +} diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 249b47fe..43f5ad76 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,10 @@ 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); + public const float VELOCITY_THRESHOLD = 0.01f; + protected override void Awake() { base.Awake(); @@ -33,92 +38,189 @@ protected override void OnDestroy() private void Server_OnTick(uint tick) { + cachedSendPacket.Tick = tick; foreach (Trainset set in Trainset.allSets) - Server_TickSet(set); - } - - private void Server_TickSet(Trainset set) - { - bool dirty = false; - foreach (TrainCar trainCar in set.cars) { - if (trainCar.isStationary) - continue; - dirty = true; - break; + if (UnloadWatcher.isUnloading || UnloadWatcher.isQuitting) + return; + + if (set != null && set.cars != null) + Server_TickSet(set, tick); + else + Multiplayer.LogWarning($"Server_OnTick(): Trainset or cars are null. Set Id: {set?.id}, Cars: {set?.cars?.Count}"); } + } + private void Server_TickSet(Trainset set, uint tick) + { + bool anyCarMoving = false; + bool maxTicksReached = false; + bool anyTracksDirty = false; - if (!dirty) + if (UnloadWatcher.isUnloading || UnloadWatcher.isQuitting) return; - cachedSendPacket.NetId = set.firstCar.GetNetId(); - - if (set.cars.Contains(null)) + if (set.firstCar == null || set.lastCar == null) { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); + Multiplayer.LogWarning($"Trainset {set?.id} has null end cars! firstCar: {set?.firstCar != null}, lastCar: {set?.lastCar != null}"); return; } + cachedSendPacket.FirstNetId = set.firstCar.GetNetId(); + cachedSendPacket.LastNetId = set.lastCar.GetNetId(); - if (set.cars.Any(car => !car.gameObject.activeSelf)) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); + //car may not be initialised, missing a valid NetID + if (cachedSendPacket.FirstNetId == 0 || cachedSendPacket.LastNetId == 0) return; + + foreach (TrainCar trainCar in set.cars) + { + if (trainCar == null || trainCar.gameObject == null || !trainCar.gameObject.activeSelf) + { + Multiplayer.LogError($"Trainset {set?.id} ({set.firstCar?.GetNetId()}) has a null or inactive car! trainCar: {trainCar != null}, gameObject: {trainCar?.gameObject != null}, active: {trainCar?.gameObject?.activeSelf}"); + 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) && netTC != null) + { + maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; //Even if the car is stationary, if the max ticks has been exceeded we will still sync + anyTracksDirty |= netTC.BogieTracksDirty; + } + else + { + Multiplayer.LogError($"NetworkedTrainCar not found for TrainCar {trainCar?.ID} in set {set?.id} ({set.firstCar?.GetNetId()})"); + return; + } + + if (trainCar.derailed) + { + if (trainCar?.rb == null) + { + Multiplayer.LogError($"Rigid body not found for TrainCar {trainCar?.ID} in set {set?.id} ({set.firstCar?.GetNetId()})"); + return; + } + + // Check if derailed car is actually moving + float velocityMagnitude = trainCar.rb.velocity.magnitude; + if (velocityMagnitude > VELOCITY_THRESHOLD) + { + anyCarMoving = true; + } + } + else if (!trainCar.isStationary) + anyCarMoving = true; + + // We can finish checking early if we have either a car moving or a car not sync'd within the max-tick threshold + if (anyCarMoving || maxTicksReached) + { + //Multiplayer.LogDebug(() => $"Server_TickSet() TrainCar {trainCar.ID} ({netTC?.NetId}) from set: {cachedSendPacket.FirstNetId} is moving or due for sync! stationary: {trainCar.isStationary}, RB velocity: {trainCar.rb.velocity} {trainCar.rb.velocity.magnitude}, tracks dirty: {netTC?.BogieTracksDirty} sync: {netTC?.TicksSinceSync >= MAX_UNSYNC_TICKS}"); + break; + } } + //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}"); + 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) + { + position = trainCar.transform.position - WorldMover.currentMove; + rotation = trainCar.transform.rotation; + + //reset this car's states + networkedTrainCar.TicksSinceSync = 0; + } + 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]), + BogieData.FromBogie(trainCar.Bogies[1]), + position, //only used in full sync + rotation //only used in full sync ); + } + + //reset this car's states + networkedTrainCar.BogieTracksDirty = false; } cachedSendPacket.TrainsetParts = trainsetParts; NetworkLifecycle.Instance.Server.SendTrainsetPhysicsUpdate(cachedSendPacket, anyTracksDirty); } - #endregion #region Client 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.TryGet(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) + { + //Multiplayer.LogDebug(()=>$"Applying TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}"); + networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + } + else + { + Multiplayer.LogWarning($"Unable to apply TrainPhysicsUpdate to {packet.TrainsetParts[i].NetId}, NetworkedTrainCar not found!"); + } + } 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); + else + Multiplayer.LogWarning($"Unable to apply TrainPhysicsUpdate to TrainSet with FirstNetId: {packet.FirstNetId}, NetworkedTrainCar not found!"); + } } - + #endregion [UsedImplicitly] diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index 6da72fd2..e8072b48 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -1,48 +1,93 @@ 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 Bogie bogie; + private const int MAX_FRAMES = 60; + public Bogie Bogie { get; private set; } + private Rigidbody rb; protected override void OnEnable() { - bogie = GetComponent(); - if (bogie == null) + StartCoroutine(WaitForBogie()); + } + + protected IEnumerator WaitForBogie() + { + int counter = 0; + + while (Bogie == null || rb == null && counter < MAX_FRAMES) { - Multiplayer.LogError($"{gameObject.name}: {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject!"); - return; + if (Bogie == null) + Bogie = GetComponent(); + + if (rb == null) + rb = GetComponent(); + + if (rb == null || 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"); + } + + if (rb == null) + { + Multiplayer.LogError($"{gameObject.name} ({Bogie?.Car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(rb)} component on the same GameObject! Waited {counter} iterations"); + } } protected override void Process(BogieData snapshot, uint snapshotTick) { - if (bogie.HasDerailed) + + //Multiplayer.LogDebug(()=>$"NetworkedBogie.Process({identifier}) DataFlags: {snapshot.DataFlags}, {snapshotTick}, {snapshot.TrackNetId}, {snapshot.PositionAlongTrack} {snapshot.TrackDirection}"); + + if (Bogie == null || rb == null || Bogie.HasDerailed) return; - if (snapshot.HasDerailed || !bogie.track) + if (snapshot.HasDerailed) { - bogie.Derail(); + Bogie.Derail(); return; } if (snapshot.IncludesTrackData) { - if (NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) - bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + if (!NetworkedRailTrack.TryGet(snapshot.TrackNetId, out NetworkedRailTrack track)) + { + Multiplayer.LogWarning($"NetworkedBogie.Process({identifier}) Failed to find track {snapshot.TrackNetId} for bogie: {Bogie.Car.ID}"); + return; + } + + if (Bogie.track != track.RailTrack) + Bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + else + Bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); } else { - bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); + if (Bogie.track) + Bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); + else + Multiplayer.LogWarning($"NetworkedBogie.Process({identifier}) 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; for (int i = 0; i < physicsSteps; i++) - bogie.UpdatePointSetTraveller(); + { + var z = transform.InverseTransformDirection(rb.velocity).z; + Bogie.UpdatePointSetTraveller(z); + } } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 4268ceb3..52ed1196 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System.Collections; +using DV.Damage; +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,24 +12,37 @@ 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++) - AutoCouple(parts[i], cars[i]); + SetBrakeParams(parts[i].BrakeData, cars[i].TrainCar); + + //couple them if marked as coupled + //- we need to do this back to front otherwise the TrainSet indicies will be wrong! + for (int i = cars.Length - 1; i >= 0; i--) + Couple(in 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) + if (!NetworkedRailTrack.TryGet(spawnPart.Bogie1.TrackNetId, out NetworkedRailTrack bogie1Track) && spawnPart.Bogie1.TrackNetId != 0) { NetworkLifecycle.Instance.Client.LogDebug(() => $"Tried spawning car but couldn't find track with index {spawnPart.Bogie1.TrackNetId}"); return null; } - if (!NetworkedRailTrack.Get(spawnPart.Bogie2.TrackNetId, out NetworkedRailTrack bogie2Track) && spawnPart.Bogie2.TrackNetId != 0) + if (!NetworkedRailTrack.TryGet(spawnPart.Bogie2.TrackNetId, out NetworkedRailTrack bogie2Track) && spawnPart.Bogie2.TrackNetId != 0) { NetworkLifecycle.Instance.Client.LogDebug(() => $"Tried spawning car but couldn't find track with index {spawnPart.Bogie2.TrackNetId}"); return null; @@ -38,24 +54,56 @@ 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); + + //set health data + if (spawnPart.Exploded) + { + var explosionBase = trainCar.GetComponent(); + if (explosionBase != null) + explosionBase.UpdateToExplodedStateExternal(); + else + TrainCarExplosion.UpdateModelToExploded(trainCar); + } + + spawnPart.CarHealthData.LoadTo(trainCar); - 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 +115,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; + } - if (!preventCoupling) - AutoCouple(spawnPart, trainCar); + 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; + } - return trainCar; + //Handle coupling at front of car + HandleCoupling(spawnPart.FrontCoupling, trainCar.frontCoupler); + + //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.TryGet(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()}"); + + 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; + } + + 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); + } - int count = trainCarList.Count; - if (count <= 0) - return Instantiate(livery); + if (!coupler.IsCoupled()) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Failed to couple, activating buffer collider"); + coupler.fakeBuffersCollider.enabled = true; + } - int index = count - 1; - TrainCar trainCar = trainCarList[index]; - trainCarList.RemoveAt(index); - CarSpawner.Instance.trainCarPoolHashSet.Remove(trainCar); + } + + private static void SetBrakeParams(BrakeSystemData brakeSystemData, TrainCar trainCar) + { + BrakeSystem bs = trainCar.brakeSystem; - if (trainCar != null) + if (bs == null) { - 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.LogWarning($"NetworkedCarSpawner.SetBrakeParams() Brake system is null! netId: {trainCar?.GetNetId()}, trainCar: {trainCar?.ID}"); + return; } - Multiplayer.LogError($"Failed to get {livery.id} from pool!"); - return Instantiate(livery); + 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 (TrainCar, bool) Instantiate(TrainCarLivery livery) + private static void BlockLoco(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; + 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 00000000..884423b6 --- /dev/null +++ b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs @@ -0,0 +1,60 @@ +using Multiplayer.Networking.Data.Train; +using System; +using System.Collections; +using UnityEngine; + +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 436649c1..78dec9bc 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,16 +1,27 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; +using DV; +using DV.CabControls; +using DV.Customization.Paint; +using DV.Damage; +using DV.HUD; +using DV.Logic.Job; +using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; +using DV.Simulation.Controllers; 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.Utils; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using UnityEngine; namespace Multiplayer.Components.Networking.Train; @@ -19,31 +30,44 @@ 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) + public static bool TryGet(ushort netId, out NetworkedTrainCar obj) { bool b = Get(netId, out IdMonoBehaviour rawObj); obj = (NetworkedTrainCar)rawObj; return b; } - public static bool GetTrainCar(ushort netId, out TrainCar obj) + public static bool TryGet(ushort netId, out TrainCar trainCar) + { + bool b = TryGet(netId, out NetworkedTrainCar networkedTrainCar); + trainCar = b ? networkedTrainCar.TrainCar : null; + return b; + } + + public static bool TryGet(ushort netId, out Car trainCar) { - bool b = Get(netId, out NetworkedTrainCar networkedTrainCar); - obj = b ? networkedTrainCar.TrainCar : null; + bool b = TryGet(netId, out NetworkedTrainCar networkedTrainCar); + trainCar = b ? networkedTrainCar.TrainCar?.logicCar : null; 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 trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); + } + public static bool GetTrainCarFromTrainId(string carId, out TrainCar trainCar) { - return trainCarsToNetworkedTrainCars[trainCar]; + return trainCarIdToTrainCars.TryGetValue(carId, out trainCar); } public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar networkedTrainCar) @@ -51,9 +75,90 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n return trainCarsToNetworkedTrainCars.TryGetValue(trainCar, out networkedTrainCar); } + public static bool TryGetNetId(TrainCar trainCar, out ushort netId) + { + netId = 0; + + if (!trainCarsToNetworkedTrainCars.TryGetValue(trainCar, out var networkedTrainCar) || networkedTrainCar == false || networkedTrainCar.NetId == 0) + return false; + + netId = networkedTrainCar.NetId; + return true; + } + + public static bool TryGetNetId(Car car, out ushort netId) + { + netId = 0; + + if (car == null || !GetFromTrainId(car.ID, out var networkedTrainCar) || networkedTrainCar == false || networkedTrainCar.NetId == 0) + return false; + + netId = networkedTrainCar.NetId; + return true; + } + + #endregion + + private const int MAX_COUPLER_ITERATIONS = 10; + private const float MAX_PORT_DELTA = 0.001f; + private const uint MIN_KINEMATIC_CYCLES = 10; + private const float DISTANCE_TOLERANCE = 2f; + private const float MAX_PAINT_DISTANCE_SQ = (CommsRadioPaintjob.SIGNAL_RANGE + DISTANCE_TOLERANCE) * (CommsRadioPaintjob.SIGNAL_RANGE + DISTANCE_TOLERANCE); + private const float POSITION_UPDATE_THRESHOLD = 0.1f; // TrainCar must have a bigger delta to apply position update + + #region Port and Fuse Map + + private static readonly Dictionary netIdToPort = []; + private static readonly Dictionary portToNetId = []; + private static readonly Dictionary netIdToFuse = []; + private static readonly Dictionary fuseToNetId = []; + + static uint GetPortNetId(string portId) + { + if (portToNetId.TryGetValue(portId, out var netId)) + return netId; + + netId = StringHashing.Fnv1aHash(portId); + + Multiplayer.LogDebug(() => $"GetPortNetId({portId}) Registering with {netId}"); + + netIdToPort[netId] = portId; + portToNetId[portId] = netId; + + return netId; + } + + static string GetPort(uint netId) + { + netIdToPort.TryGetValue(netId, out var portId); + return portId; + } + + static uint GetFuseNetId(string fuseId) + { + if (fuseToNetId.TryGetValue(fuseId, out var netId)) + return netId; + + netId = StringHashing.Fnv1aHash(fuseId); + netIdToFuse[netId] = fuseId; + fuseToNetId[fuseId] = netId; + + return netId; + } + static string GetFuse(uint netId) + { + netIdToFuse.TryGetValue(netId, out var portId); + return portId; + } #endregion + public float CarLengthSq => CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery] * CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery]; + + public string CurrentID { get; private set; } public TrainCar TrainCar; + public uint TicksSinceSync = uint.MaxValue; + + public uint lastTickProcessed = 0; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; private Bogie bogie1; @@ -62,29 +167,65 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool hasSimFlow; private SimulationFlow simulationFlow; + SimController simController; + public FireboxSimController firebox; + public CoalPileSimController coalPile; + private readonly Dictionary trainDamageDelegates = []; + + private HashSet dirtyPorts; + private Dictionary lastSentPortValues; + private HashSet dirtyFuses; + private readonly HashSet dirtyPaints = []; + private readonly Dictionary lastSentTrainDamages = []; - private HashSet dirtyPorts; - private Dictionary lastSentPortValues; - private HashSet dirtyFuses; private bool handbrakeDirty; + private bool mainResPressureDirty; + private bool brakeOverheatDirty; + public bool BogieTracksDirty; - public int Bogie1TrackDirection; - public int Bogie2TrackDirection; - private bool cargoDirty; + private bool cargoStateDirty; + private bool cargoHealthDirty; private bool cargoIsLoading; public byte CargoModelIndex = byte.MaxValue; - private bool healthDirty; + private bool carHealthDirty; private bool sendCouplers; + private bool sendCables; public bool IsDestroying; - #region Client + #region Server Variables + //Coupler interaction + private bool frontInteracting = false; + private bool rearInteracting = false; - private bool client_Initialized; + private ServerPlayer frontInteractionPlayer; + private ServerPlayer rearInteractionPlayer; + + private readonly Dictionary portAuthority = []; + + #endregion + + #region Client Variables + + 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; + + private uint kinematicCycles = 0; + + private readonly Dictionary portNetIdToBlockState = []; + private readonly Dictionary portNetIdToControl = []; + private readonly Dictionary controlToPortNetId = []; + #endregion + + #region Common Variables #endregion @@ -97,12 +238,15 @@ protected override void Awake() TrainCar = GetComponent(); trainCarsToNetworkedTrainCars[TrainCar] = this; + TrainCar.LogicCarInitialized += OnLogicCarInitialised; + bogie1 = TrainCar.Bogies[0]; bogie2 = TrainCar.Bogies[1]; if (NetworkLifecycle.Instance.IsHost()) { NetworkTrainsetWatcher.Instance.CheckInstance(); // Ensure the NetworkTrainsetWatcher is initialized + Client_Initialized = true; } else { @@ -112,61 +256,309 @@ protected override void Awake() } } - private void Start() + [UsedImplicitly] + public void Start() { brakeSystem = TrainCar.brakeSystem; + Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId})"); + foreach (Coupler coupler in TrainCar.couplers) + { hoseToCoupler[coupler.hoseAndCock] = coupler; - SimController simController = GetComponent(); + //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); }; + } + + Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({CurrentID}, {NetId}) Couplers complete"); + + simController = GetComponent(); if (simController != null) { hasSimFlow = true; simulationFlow = simController.SimulationFlow; - dirtyPorts = new HashSet(simulationFlow.fullPortIdToPort.Count); - lastSentPortValues = new Dictionary(dirtyPorts.Count); + TrainCar.InteriorLoaded += OnTrainCarInteriorLoaded; + TrainCar.InteriorAboutToBeUnloaded += OnTrainCarInteriorUnloaded; + + if (TrainCar.loadedInterior != null) + OnTrainCarInteriorLoaded(TrainCar.loadedInterior.gameObject); + + dirtyPorts = new HashSet(simulationFlow.fullPortIdToPort.Count); + lastSentPortValues = new Dictionary(dirtyPorts.Count); foreach (KeyValuePair kvp in simulationFlow.fullPortIdToPort) + { + _ = GetPortNetId(kvp.Key); //ensure this port is registered if (kvp.Value.valueType == PortValueType.CONTROL || NetworkLifecycle.Instance.IsHost()) + { + Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({CurrentID}, {NetId}) Subscribing to port {kvp.Key}"); kvp.Value.ValueUpdatedInternally += _ => { Common_OnPortUpdated(kvp.Value); }; + } + } - dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); + dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); foreach (KeyValuePair kvp in simulationFlow.fullFuseIdToFuse) + { + _ = GetFuseNetId(kvp.Key); //ensure this fuse is registered kvp.Value.StateUpdated += _ => { Common_OnFuseUpdated(kvp.Value); }; + } + + firebox = simController.firebox; + coalPile = simController.coalPile; + + // Ports pulsed on an event (adding coal, igniting firebox, etc) + if (firebox != null) + { + firebox.fireboxCoalControlPort.ValueUpdatedInternally += Client_OnFireboxAddCoal; //Player adding coal + firebox.fireboxIgnitionPort.ValueUpdatedInternally += Client_OnIgnite; //Player igniting firebox + } + + if (coalPile != null) + { + coalPile.coalConsumePort.ValueUpdatedInternally += Client_OnCoalPileInteraction; //Coal being added/removed by shovel or feeder + } } + //Multiplayer.LogDebug(() => $"NetworkedTrainCar.Start({TrainCar?.ID}, {NetId}) SimController complete"); + 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.PlayerDisconnected += Server_OnPlayerDisconnect; + bogie1.TrackChanged += Server_BogieTrackChanged; bogie2.TrackChanged += Server_BogieTrackChanged; + + TrainCar.frontCoupler.Uncoupled += Server_CouplerUncoupled; + TrainCar.rearCoupler.Uncoupled += Server_CouplerUncoupled; + TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; + + //find all TrainDamages and subscribe + if (TryGetComponent(out DamageController damageController) && damageController != null) + { + var trainDamageFields = typeof(DamageController) + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(field => field.FieldType == typeof(TrainDamage)) + .Select(field => new { Field = field, Damage = (TrainDamage)field.GetValue(damageController) }) + .Where(value => value.Damage != null) + .ToArray(); + + if (trainDamageFields != null && trainDamageFields.Length > 0) + { + for (int i = 0; i < trainDamageFields.Length; i++) + { + var fieldName = trainDamageFields[i].Field.Name; + var fieldValue = trainDamageFields[i].Damage; + + //create a delegate for each field + void DamagesUpdate(float health) => Server_TrainDamagesHealthUpdate(fieldName, health); + + //subscribe to the event + trainDamageFields[i].Damage.HealthPercentageChanged += DamagesUpdate; + + //store delegates and set a last sent value to an impossible value + trainDamageDelegates.Add(fieldValue, DamagesUpdate); + lastSentTrainDamages.Add(fieldName, -1f); + } + } + } + + brakeSystem.MainResPressureChanged += Server_MainResUpdate; + brakeSystem.heatController.OverheatingActiveStateChanged += Server_BrakeHeatUpdate; + StartCoroutine(Server_WaitForLogicCar()); } + + NetworkLifecycle.Instance?.Client.SendTrainSyncRequest(NetId); + } + + private void OnTrainCarInteriorLoaded(GameObject interior) + { + Multiplayer.LogDebug(() => $"OnTrainCarInteriorLoaded() {CurrentID}, interior is null: {interior == null}"); + + StartCoroutine(WaitForInterior()); + } + + private IEnumerator WaitForInterior() + { + float time = Time.time; + InteriorControlsManager interiorControlsManager = null; + + yield return new WaitUntil + ( + () => + { + return TrainCar.loadedInterior != null || Time.time - time > 2000f; + } + ); + + yield return new WaitForFixedUpdate(); + + if (TrainCar.loadedInterior == null) + { + Multiplayer.LogError($"TrainCar {CurrentID} failed to load an interior"); + yield break; + } + + time = Time.time; + + yield return new WaitUntil + ( + () => + { + return TrainCar.loadedInterior.TryGetComponent(out interiorControlsManager) || Time.time - time > 2000f; + } + ); + + yield return new WaitForFixedUpdate(); + + if (!interiorControlsManager.Initialized) + { + interiorControlsManager.OnInitialized += HookControls; + yield break; + } + + yield return new WaitForSecondsRealtime(2f); + + HookControls(interiorControlsManager); + } + + private void HookControls(InteriorControlsManager interiorControlsManager) + { + interiorControlsManager.OnInitialized -= HookControls; + + // Find all control overrides + foreach (var control in interiorControlsManager.controls.Values) + { + var controlPortId = control.overridableBaseControl?.portId; + + if (string.IsNullOrEmpty(controlPortId)) + { + Multiplayer.LogDebug(() => $"HookControls() Control, {NetId}] has no controlPortId on car {CurrentID}"); + continue; + } + + Multiplayer.LogDebug(() => $"HookControls() Control [{controlPortId}] found on car {CurrentID}"); + var netId = GetPortNetId(controlPortId); + + + if (control.controlImplBase == null) + { + Multiplayer.LogDebug(() => $"HookControls() Control [{controlPortId}, {netId}] has no implementation on car {CurrentID}"); + continue; + } + + Multiplayer.LogDebug(() => $"HookControls() Control [{controlPortId}, {netId}] hooking events on car {CurrentID}, hash: {control.controlImplBase.GetHashCode()}, instance: {control.controlImplBase.GetInstanceID()}"); + + portNetIdToControl[netId] = control.controlImplBase; + controlToPortNetId[control.controlImplBase] = netId; + + control.controlImplBase.Grabbed += Client_ControlGrabbed; + control.controlImplBase.Ungrabbed += Client_ControlUngrabbed; + + if (portNetIdToBlockState.TryGetValue(netId, out var isBlocked) && isBlocked) + { + Multiplayer.LogDebug(() => $"WaitForInterior() Control [{controlPortId}, {netId}] is blocked on car {CurrentID}"); + } + } + } + + private void OnTrainCarInteriorUnloaded(GameObject interior) + { + Multiplayer.LogDebug(() => $"OnTrainCarInteriorUnloaded() {CurrentID}"); + + foreach (var control in controlToPortNetId.Keys) + { + if (control == null) + continue; + + control.Grabbed -= Client_ControlGrabbed; + control.Ungrabbed -= Client_ControlUngrabbed; + } + + portNetIdToControl.Clear(); + controlToPortNetId.Clear(); } - private void OnDisable() + + public void OnDisable() { if (UnloadWatcher.isQuitting) return; - NetworkLifecycle.Instance.OnTick -= Common_OnTick; - NetworkLifecycle.Instance.OnTick -= Server_OnTick; - if (UnloadWatcher.isUnloading) - return; + + //Clean dictionaries 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; + + //stop tracking client events + NetworkLifecycle.Instance.OnTick -= Common_OnTick; + + if (firebox != null) + { + firebox.fireboxCoalControlPort.ValueUpdatedInternally -= Client_OnFireboxAddCoal; //Player adding coal + firebox.fireboxIgnitionPort.ValueUpdatedInternally -= Client_OnIgnite; //Player igniting firebox + } + + if (coalPile != null) + { + coalPile.coalConsumePort.ValueUpdatedInternally -= Client_OnCoalPileInteraction; //Coal being added/removed by shovel or feeder + } + + 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; + + //stop tracking server events if (NetworkLifecycle.Instance.IsHost()) { + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + NetworkLifecycle.Instance.Server.PlayerDisconnected -= Server_OnPlayerDisconnect; + bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; + + TrainCar.frontCoupler.Uncoupled -= Server_CouplerUncoupled; + TrainCar.rearCoupler.Uncoupled -= Server_CouplerUncoupled; + TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; + + //Unsubscribe from damage updates + if (trainDamageDelegates != null && lastSentTrainDamages.Count > 0) + foreach (var kvp in trainDamageDelegates) + kvp.Key.HealthPercentageChanged -= kvp.Value; + + if (brakeSystem != null) + { + brakeSystem.MainResPressureChanged -= Server_MainResUpdate; + brakeSystem.heatController.OverheatingActiveStateChanged -= Server_BrakeHeatUpdate; + } + if (TrainCar.logicCar != null) { TrainCar.logicCar.CargoLoaded -= Server_OnCargoLoaded; @@ -174,52 +566,92 @@ 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); + + if (TrainCar.CargoDamage) + TrainCar.CargoDamage.CargoEffectiveHealthStateUpdate += Server_CargoHealthUpdate; + + Server_DirtyAllState(); } public void Server_DirtyAllState() { handbrakeDirty = true; - cargoDirty = true; + mainResPressureDirty = true; + cargoStateDirty = true; + cargoHealthDirty = true; cargoIsLoading = true; - healthDirty = true; + carHealthDirty = true; BogieTracksDirty = true; sendCouplers = true; + sendCables = true; + if (!hasSimFlow) return; foreach (string portId in simulationFlow.fullPortIdToPort.Keys) { - dirtyPorts.Add(portId); - if (simulationFlow.TryGetPort(portId, out Port port)) - lastSentPortValues[portId] = port.value; + var netId = GetPortNetId(portId); + dirtyPorts.Add(netId); } foreach (string fuseId in simulationFlow.fullFuseIdToFuse.Keys) - dirtyFuses.Add(fuseId); + { + var netId = GetFuseNetId(fuseId); + dirtyFuses.Add(netId); + } } public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainPortsPacket packet) { // 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) + foreach (uint portNetId in packet.PortIds) + { + + var portId = GetPort(portNetId); + if (simulationFlow.TryGetPort(portId, out Port port)) + { + if (port.valueType != PortValueType.CONTROL) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port! ([{portId}, {portNetId}] on [{CurrentID}, {NetId}])"); + Common_DirtyPorts(packet.PortIds); + return false; + } + } + else { - NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port!"); - Common_DirtyPorts(packet.PortIds); - return false; + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portNetId}, value type: {port.valueType}, but the port was not found"); } + } // Only allow the player to update ports on the car they are in/near if (player.CarId == packet.NetId) @@ -227,8 +659,7 @@ public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainP // Some ports can be updated by the player even if they are not in the car, like doors and windows. // Only deny the request if the player is more than 5 meters away from any point of the car. - float carLength = CarSpawner.Instance.carLiveryToCarLength[TrainCar.carLivery]; - if ((player.WorldPosition - transform.position).sqrMagnitude <= carLength * carLength) + if ((player.WorldPosition - transform.position).sqrMagnitude <= CarLengthSq) return true; NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a sim flow packet for a car they are not in!"); @@ -243,119 +674,376 @@ private void Server_BogieTrackChanged(RailTrack arg1, Bogie arg2) private void Server_OnCargoLoaded(CargoType obj) { - cargoDirty = true; + cargoStateDirty = true; cargoIsLoading = true; } private void Server_OnCargoUnloaded() { - cargoDirty = true; + cargoStateDirty = true; cargoIsLoading = false; CargoModelIndex = byte.MaxValue; } + private void Server_CargoHealthUpdate(float health) + { + cargoHealthDirty = true; + } + private void Server_CarHealthUpdate(float health) { - healthDirty = true; + //Multiplayer.LogDebug(() => $"Server_CarHealthUpdate({health}) netId: {NetId}"); + carHealthDirty = true; + } + + private void Server_TrainDamagesHealthUpdate(string field, float health) + { + //Multiplayer.LogDebug(() => $"Server_TrainDamagesHealthUpdate({field}, {health}) netId: {NetId}"); + + // Check if value has changed before updating + if (!lastSentTrainDamages.TryGetValue(field, out float lastValue) + || Mathf.Abs(lastValue - health) > MAX_PORT_DELTA + || (health == 0 && lastValue != 0) + || (health == 1 && lastValue != 1)) + { + lastSentTrainDamages[field] = health; + carHealthDirty = true; + } + } + + private void Server_MainResUpdate(float normalizedPressure, float pressure) + { + mainResPressureDirty = true; + } + + private void Server_BrakeHeatUpdate(bool overheatActive) + { + brakeOverheatDirty = true; + } + + private void Server_CouplerUncoupled(object _, UncoupleEventArgs args) + { + sendCouplers |= args.dueToBrokenCouple; } private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; + + Server_SendBrakeStates(); Server_SendCouplers(); + Server_SendCables(); Server_SendCargoState(); - Server_SendHealthState(); + Server_SendCargoHealthUpdate(); + Server_SendCarHealthState(); + + 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_SendCouplers() { if (!sendCouplers) return; + sendCouplers = false; - if (TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) - NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); + if (!TrainCar.frontCoupler.IsCoupled()) + // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.frontCoupler,TrainCar.frontCoupler.coupledTo,false, false); + //else + NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.frontCoupler, true, true, false); - if (TrainCar.rearCoupler.hoseAndCock.IsHoseConnected) - NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); + if (!TrainCar.rearCoupler.IsCoupled()) + // NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.rearCoupler,TrainCar.rearCoupler.coupledTo,false, false); + //else + NetworkLifecycle.Instance.Server.SendTrainUncouple(TrainCar.rearCoupler, true, true, false); - NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); - NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); + if (!TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) + // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); + //else + NetworkLifecycle.Instance.Server.SendHoseDisconnected(TrainCar.frontCoupler, true); + + if (!TrainCar.rearCoupler.hoseAndCock.IsHoseConnected) + // NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.rearCoupler, TrainCar.rearCoupler.coupledTo, false); + //else + NetworkLifecycle.Instance.Server.SendHoseDisconnected(TrainCar.rearCoupler, true); + + NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); + NetworkLifecycle.Instance.Server.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); } - private void Server_SendCargoState() + private void Server_SendCables() { - if (!cargoDirty) + if (!sendCables) return; - cargoDirty = false; - if (cargoIsLoading && TrainCar.logicCar.CurrentCargoTypeInCar == CargoType.None) + sendCables = false; + + if (TrainCar.muModule == null) return; - NetworkLifecycle.Instance.Server.SendCargoState(TrainCar, NetId, cargoIsLoading, CargoModelIndex); + + 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_SendHealthState() + private void Server_SendCargoState() { - if (!healthDirty) + if (!cargoStateDirty) return; - healthDirty = false; - NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCar.CarDamage.currentHealth); + cargoStateDirty = false; + if (cargoIsLoading && TrainCar.logicCar.CurrentCargoTypeInCar == CargoType.None) + return; + + NetworkLifecycle.Instance.Server.SendCargoState(this, cargoIsLoading, CargoModelIndex); } - #endregion + private void Server_SendCargoHealthUpdate() + { + if (!cargoHealthDirty) + return; - #region Common + cargoHealthDirty = false; - private void Common_OnTick(uint tick) - { - if (UnloadWatcher.isUnloading) + if (TrainCar.logicCar.CurrentCargoTypeInCar == CargoType.None) return; - Common_SendHandbrakePosition(); - Common_SendFuses(); - Common_SendPorts(); + + NetworkLifecycle.Instance.Server.SendCargoHealthUpdate(NetId, TrainCar.CargoDamage.currentHealth); } - private void Common_SendHandbrakePosition() + private void Server_SendCarHealthState() { - if (!handbrakeDirty) + if (!carHealthDirty) return; - if (!TrainCar.brakeSystem.hasHandbrake) - return; - handbrakeDirty = false; - NetworkLifecycle.Instance.Client.SendHandbrakePositionChanged(NetId, brakeSystem.handbrakePosition); + carHealthDirty = false; + NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCarHealthData.From(TrainCar)); } - public void Common_DirtyPorts(string[] portIds) + public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ServerPlayer player) + { + Multiplayer.LogDebug(() => + $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {player.PlayerId}) " + + $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPlayer}, " + + $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPlayer}" + ); + + //Ensure no one else is interacting + if (packet.IsFrontCoupler && frontInteracting && player != frontInteractionPlayer || + packet.IsFrontCoupler == false && rearInteracting && player != rearInteractionPlayer) + { + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.PlayerId}) Failed to validate!"); + return false; + } + + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {player.PlayerId}) No one interacting"); + + if (((CouplerInteractionType)packet.Flags).HasFlag(CouplerInteractionType.Start)) + { + if (packet.IsFrontCoupler) + { + frontInteracting = true; + frontInteractionPlayer = player; + } + else + { + rearInteracting = true; + rearInteractionPlayer = player; + } + } + 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}], {player.PlayerId}) Validation passed!"); + return true; + } + + public void Server_ReceiveAuthorityRequest(uint portNetId, ServerPlayer player, bool requestAuthority) + { + portAuthority.TryGetValue(portNetId, out var currentAuth); + + if (requestAuthority) + { + if (currentAuth == null) + { + if ((player.WorldPosition - transform.position).sqrMagnitude > CarLengthSq) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to gain authority for a control on car {CurrentID}, but they are too far away!"); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Released, player); + return; + } + + // No authority exists (or cleanup failed) - grant authority and communicate to all players + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player \"{player.Username}\" granted authority for a control on car {CurrentID}"); + portAuthority[portNetId] = player; + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Blocked, excludePlayer: player); + } + else if (currentAuth != player) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to gain authority for a control that's in use on car {CurrentID}"); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); + } + } + else + { + // Release request + if (currentAuth == player) + { + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player \"{player.Username}\" released authority for a control on car {CurrentID}"); + portAuthority.Remove(portNetId); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Released); + } + else if (currentAuth != null) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to release authority for a control that's not theirs on car {CurrentID}"); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, portNetId, ControlAuthorityState.Denied, player); + } + } + } + + public void Server_ValidatePaintThemeChange(TrainCarPaint.Target target, PaintTheme theme, ServerPlayer player) + { + var playerPos = TrainCar.transform.InverseTransformPoint(player.WorldPosition); + Vector3 localClosestPoint = TrainCar.Bounds.ClosestPoint(playerPos); + Vector3 worldClosestPoint = TrainCar.transform.TransformPoint(localClosestPoint); + + if ((player.WorldPosition - worldClosestPoint).sqrMagnitude > MAX_PAINT_DISTANCE_SQ) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player \"{player.Username}\" attempted to change the paint theme for car {CurrentID}, but they are too far away!"); + + var paintControllers = GetComponents(); + int i = 0; + while (i < paintControllers.Length) + { + if (paintControllers[i].TargetArea == target) + break; + + i++; + } + + if (i >= paintControllers.Length) + return; + + if (!PaintThemeLookup.Instance.TryGetNetId(paintControllers[i].CurrentTheme, out var themeNetId)) + { + Multiplayer.LogWarning($"ValidatePaintThemeChange could not find themeNetId for theme {paintControllers[i].CurrentTheme.AssetName} on [{CurrentID}, {NetId}]"); + return; + } + + NetworkLifecycle.Instance.Server.SendPaintThemeChange(this, target, themeNetId, player); + + return; + } + + Common_ReceivePaintThemeUpdate(target, theme); + } + + private void Server_OnPlayerDisconnect(ServerPlayer player) + { + //todo: resolve player disconnection during chain interaction + if (frontInteractionPlayer == player || rearInteractionPlayer == player) + { + Multiplayer.LogWarning($"Server_OnPlayerDisconnect() Coupler interaction in unknown state [{CurrentID}, {NetId}] isFront: {frontInteractionPlayer == player}"); + if (frontInteractionPlayer == player) + { + frontInteracting = false; + //NetworkLifecycle.Instance.Client.SendCouplerInteraction(cou, coupler, otherCoupler); + } + else + { + rearInteracting = false; + } + } + + // Clean up blocked controls + foreach (var kvp in portAuthority.Where(kvp => kvp.Value == player)) + { + portAuthority.Remove(kvp.Key); + NetworkLifecycle.Instance.Server.SendTrainControlAuthorityUpdate(NetId, kvp.Key, ControlAuthorityState.Released); + } + } + #endregion + + #region Common + + private void Common_OnTick(uint tick) + { + if (UnloadWatcher.isUnloading) + return; + + Common_SendHandbrakePosition(); + Common_SendFuses(); + Common_SendPorts(); + Common_SendPaintThemes(); + } + + private void Common_SendHandbrakePosition() + { + if (!handbrakeDirty) + return; + if (!TrainCar.brakeSystem.hasHandbrake) + return; + + handbrakeDirty = false; + NetworkLifecycle.Instance.Client.SendHandbrakePositionChanged(NetId, brakeSystem.handbrakePosition); + } + + public void Common_DirtyPorts(uint[] portNetIds) { if (!hasSimFlow) return; - foreach (string portId in portIds) + foreach (uint portNetId in portNetIds) { + var portId = GetPort(portNetId); if (!simulationFlow.TryGetPort(portId, out Port _)) { - Multiplayer.LogWarning($"Tried to dirty port {portId} on {TrainCar.ID} but it doesn't exist!"); + Multiplayer.LogWarning($"Tried to dirty port [{portId}, {portNetId}] on {CurrentID} but port doesn't exist!"); continue; } - dirtyPorts.Add(portId); + dirtyPorts.Add(portNetId); } } - public void Common_DirtyFuses(string[] fuseIds) + public void Common_DirtyFuses(uint[] fuseNetIds) { if (!hasSimFlow) return; - foreach (string fuseId in fuseIds) + foreach (uint fuseNetId in fuseNetIds) { + var fuseId = GetFuse(fuseNetId); if (!simulationFlow.TryGetFuse(fuseId, out Fuse _)) { - Multiplayer.LogWarning($"Tried to dirty port {fuseId} on {TrainCar.ID} but it doesn't exist!"); + Multiplayer.LogWarning($"Tried to dirty port [{fuseId}, {fuseNetId}] on {CurrentID} but it doesn't exist!"); continue; } - dirtyFuses.Add(fuseId); + dirtyFuses.Add(fuseNetId); } } @@ -365,13 +1053,23 @@ private void Common_SendPorts() return; int i = 0; - string[] portIds = dirtyPorts.ToArray(); + uint[] portIds = dirtyPorts.ToArray(); float[] portValues = new float[portIds.Length]; - foreach (string portId in dirtyPorts) + foreach (uint portNetId in dirtyPorts) { - float value = simulationFlow.fullPortIdToPort[portId].Value; - portValues[i++] = value; - lastSentPortValues[portId] = value; + var portId = GetPort(portNetId); + if (simulationFlow.TryGetPort(portId, out Port port)) + { + float value = port.Value; + portValues[i] = value; + lastSentPortValues[portNetId] = value; + } + else + { + Multiplayer.LogWarning($"Failed to send port \"{portId}\" for [{CurrentID}, {NetId}]"); + } + + i++; } dirtyPorts.Clear(); @@ -385,16 +1083,48 @@ private void Common_SendFuses() return; int i = 0; - string[] fuseIds = dirtyFuses.ToArray(); + uint[] fuseIds = dirtyFuses.ToArray(); bool[] fuseValues = new bool[fuseIds.Length]; - foreach (string fuseId in dirtyFuses) - fuseValues[i++] = simulationFlow.fullFuseIdToFuse[fuseId].State; + + foreach (uint fuseNetId in dirtyFuses) + { + var fuseId = GetFuse(fuseNetId); + if (simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) + fuseValues[i] = fuse.State; + else + Multiplayer.LogWarning($"Failed to send fuse \"{fuseId}\" for [{CurrentID}, {NetId}]"); + + i++; + } dirtyFuses.Clear(); NetworkLifecycle.Instance.Client.SendFuses(NetId, fuseIds, fuseValues); } + private void Common_SendPaintThemes() + { + if (dirtyPaints.Count == 0) + return; + + //Multiplayer.LogDebug(() => $"Common_SendPaintThemes() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); + foreach (var paintController in dirtyPaints) + { + if (!PaintThemeLookup.Instance.TryGetNetId(paintController.CurrentTheme, out var themeNetId)) + { + Multiplayer.LogWarning($"Common_SendPaintThemes() could not find themeId for theme {paintController.CurrentTheme.name} on [{CurrentID}, {NetId}]"); + return; + } + + if (NetworkLifecycle.Instance.IsHost()) + NetworkLifecycle.Instance.Server.SendPaintThemeChange(this, paintController.targetArea, themeNetId); + else + NetworkLifecycle.Instance.Client.SendPaintThemeChange(this, paintController.targetArea, themeNetId); + } + + dirtyPaints.Clear(); + } + private void Common_OnHandbrakePositionChanged((float, bool) data) { if (NetworkLifecycle.Instance.IsProcessingPacket) @@ -411,20 +1141,61 @@ private void Common_OnBrakeCylinderReleased() private void Common_OnPortUpdated(Port port) { + + if (port.valueType != PortValueType.CONTROL && !NetworkLifecycle.Instance.IsHost()) + { + Multiplayer.LogDebug(() => $"Common_OnPortUpdated() Ignoring non-control port update for [{port.id}] on [{CurrentID}, {NetId}]"); + return; + } + 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) + + var netId = GetPortNetId(port.id); + bool hasLastSent = lastSentPortValues.TryGetValue(netId, out float lastSentValue); + float delta = Mathf.Abs(lastSentValue - port.Value); + + if (port.valueType == PortValueType.STATE) + { + if (!hasLastSent || lastSentValue != port.Value) + { + dirtyPorts.Add(netId); + } + } + else + { + if (!hasLastSent || delta > MAX_PORT_DELTA || (port.Value == 0 && lastSentValue != 0)) + { + dirtyPorts.Add(netId); + } + + if (port.valueType == PortValueType.CONTROL) + { + dirtyPorts.Add(netId); + } + } + } + + private void Common_OnPaintThemeChange(TrainCarPaint paintController) + { + if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - dirtyPorts.Add(port.id); + + if (paintController == null) + return; + + dirtyPaints.Add(paintController); } private void Common_OnFuseUpdated(Fuse fuse) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - dirtyFuses.Add(fuse.id); + + var netId = GetFuseNetId(fuse.id); + dirtyFuses.Add(netId); } public void Common_UpdatePorts(CommonTrainPortsPacket packet) @@ -434,12 +1205,20 @@ 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); + var portId = GetPort(packet.PortIds[i]); + if (simulationFlow.TryGetPort(portId, 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($"Failed to update port [\"portId\", {packet.PortIds[i]}] with value \"{packet.PortValues[i]}\" for [{CurrentID}, {NetId}]"); + } } } @@ -449,9 +1228,391 @@ 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]); + { + var fuseId = GetFuse(packet.FuseIds[i]); + if (simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) + fuse.ChangeState(packet.FuseValues[i]); + else + Multiplayer.LogWarning($"Failed to update fuse [\"fuseId\", {packet.FuseIds[i]}] with value \"{packet.FuseValues[i]}\" for [{CurrentID}, {NetId}]"); + } + } + + 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; + + ButtonBase buttonBase = coupler?.ChainScript?.screwButton.GetComponent(); + + 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 (TryGet(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 (buttonBase) + buttonBase.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 (buttonBase) + buttonBase.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.LogDebug(() => $"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Interior"); + targetPaint = TrainCar.PaintInterior; + } + else if (target == TrainCarPaint.Target.Exterior) + { + Multiplayer.LogDebug(() => $"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 +1623,276 @@ 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 (tick <= lastTickProcessed) { + Multiplayer.LogWarning($"Received physics update for car {CurrentID} at tick {tick}, but last tick processed was {lastTickProcessed}"); + return; + } + + lastTickProcessed = tick; + + if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) + { + //Vector3 expectedPosition = movementPart.RigidbodySnapshot.Position + WorldMover.currentMove; + //Multiplayer.LogDebug(() => $"Processing derailed physics for car {CurrentID} at tick {tick}, current position: {TrainCar.transform.position} expected position: {expectedPosition}"); + + if (TrainCar.isEligibleForSleep) + TrainCar.ForceOptimizationState(false); + TrainCar.Derail(); - TrainCar.stress.ResetTrainStress(); - Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + movementPart.RigidbodySnapshot.Apply(TrainCar.rb); + + // Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); + + //Multiplayer.LogDebug(() => $"Derailed car {TrainCar.ID} positioned at {TrainCar.transform.position}"); } else { + // Only force awake if there's movement + if (Mathf.Abs(movementPart.Speed) > NetworkTrainsetWatcher.VELOCITY_THRESHOLD && TrainCar.isEligibleForSleep) + TrainCar.ForceOptimizationState(false); + + //move the car to the correct position first - maybe? + if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Position)) + { + Vector3 worldPos = movementPart.Position + WorldMover.currentMove; + + // Only apply position update if change exceeds threshold (stops cariages from jittering while stationary) + float positionDelta = Vector3.Distance(TrainCar.transform.position, worldPos); + + if (positionDelta > POSITION_UPDATE_THRESHOLD) + { + if (TrainCar.rb != null) + { + TrainCar.rb.MovePosition(worldPos); + + //TrainCar.rb.MoveRotation(movementPart.Rotation); // removed due to motion sickness issues + } + + //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); } + + bool kinematic = movementPart.Speed < NetworkTrainsetWatcher.VELOCITY_THRESHOLD && (movementPart.RigidbodySnapshot != null && movementPart.RigidbodySnapshot.Velocity.magnitude < NetworkTrainsetWatcher.VELOCITY_THRESHOLD); + + if (kinematic && kinematicCycles < MIN_KINEMATIC_CYCLES) + kinematicCycles++; + else + TrainCar.rb.isKinematic = kinematic; + + if (!kinematic) + { + kinematicCycles = 0; + TrainCar.rb.isKinematic = kinematic; + } + } + + 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_OnFireboxAddCoal(float coalMassDelta) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (coalMassDelta <= 0) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Client_OnFireboxAddCoal({CurrentID}): 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({CurrentID})"); + NetworkLifecycle.Instance.Client.SendFireboxIgnition(NetId); + } + + private void Client_OnCoalPileInteraction(float coalMassDelta) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Client_OnCoalPileInteraction({CurrentID}): coalMassDelta: {coalMassDelta}"); + NetworkLifecycle.Instance.Client.SendTenderCoalPileInteraction(NetId, coalMassDelta); + } + + 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}]"); } + private void Client_ControlGrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"Client_ControlGrabbed() Control {control.name}, car: {CurrentID}"); + if (!controlToPortNetId.TryGetValue(control, out var portNetId)) + { + Multiplayer.LogWarning($"Control \"{control.name}\" grabbed but netId not found on TrainCar \"{CurrentID}\", hash: {control.GetHashCode()}, instance: {control.GetInstanceID()}"); + return; + } + + if (portNetIdToBlockState.TryGetValue(portNetId, out var isBlocked) && isBlocked) + { + Multiplayer.LogDebug(() => $"Client_ControlGrabbed() Control [{control.name}, {portNetId}] is blocked on car {CurrentID}, ending interaction"); + control.ForceEndInteraction(); + } + else + { + Multiplayer.LogDebug(() => $"Client_ControlGrabbed() Control [{control.name}, {portNetId}] is not blocked on car {CurrentID}, requesting authority"); + NetworkLifecycle.Instance.Client?.SendTrainControlAuthorityRequest(NetId, portNetId, true); + } + } + + private void Client_ControlUngrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"Client_ControlUngrabbed() Control {control.name}, car: {CurrentID}"); + if (!controlToPortNetId.TryGetValue(control, out var portNetId)) + { + Multiplayer.LogWarning($"Control \"{control.name}\" ungrabbed but netId not found on TrainCar \"{CurrentID}\""); + return; + } + + if (!portNetIdToBlockState.ContainsKey(portNetId)) + portNetIdToBlockState[portNetId] = false; + + if (portNetIdToBlockState.TryGetValue(portNetId, out var isBlocked) && !isBlocked) + { + Multiplayer.LogDebug(() => $"Client_ControlUngrabbed() Control [{control.name}, {portNetId}] not blocked, releasing authority for car {CurrentID}"); + NetworkLifecycle.Instance.Client?.SendTrainControlAuthorityRequest(NetId, portNetId, false); + } + } + + public void Client_ReceiveAuthorityUpdate(uint portNetId, ControlAuthorityState state) + { + bool shouldBlock = state == ControlAuthorityState.Blocked || state == ControlAuthorityState.Denied; + portNetIdToBlockState[portNetId] = shouldBlock; + + Multiplayer.LogDebug(() => $"Client_ReceiveAuthorityUpdate({portNetId}, {state}) for [{CurrentID}, {NetId}]"); + + if (!portNetIdToControl.TryGetValue(portNetId, out var control) || control == null) + return; + + if (shouldBlock) + { + control.ForceEndInteraction(); + control.BlockControl(true); + control.InteractionAllowed = false; + } + else + { + control.BlockControl(false); + control.InteractionAllowed = true; + } + } #endregion } diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs new file mode 100644 index 00000000..40d12dd0 --- /dev/null +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -0,0 +1,174 @@ +using DV.Customization.Paint; +using DV.Utils; +using JetBrains.Annotations; +using Multiplayer.Utils; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Components.Networking.Train; + +public class PaintThemeLookup : SingletonBehaviour +{ + private readonly Dictionary hashToThemeName = []; + private readonly Dictionary themeNameToHash = []; + private readonly Dictionary hashToBaseThemeName = []; + private readonly HashSet baseThemeNamesSet = []; + + private bool moddedThemesSearched = false; + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(PaintThemeLookup)}]"; + } + + protected override void Awake() + { + base.Awake(); + + var themes = Resources.LoadAll(""); + + foreach (var theme in themes) + { + if (theme != null && !string.IsNullOrEmpty(theme.AssetName)) + { + var id = RegisterTheme(theme); + if (id != 0) + hashToBaseThemeName[id] = theme.AssetName; + } + } + + baseThemeNamesSet.UnionWith(hashToBaseThemeName.Values); + } + + #region Public Methods + public bool TryGetNetId(PaintTheme theme, out uint netId) + { + netId = 0; + + if (theme == null) + return false; + + if (themeNameToHash.TryGetValue(theme.AssetName, out netId)) + return true; + + netId = GetThemeId(theme.AssetName); + + if (hashToThemeName.ContainsKey(netId)) + return true; + + // Skin Manager might not have been updated for Multiplayer yet, or another mod may have added themes + // Try to find themes added by mods + FindModdedThemes(); + + if (hashToThemeName.ContainsKey(netId)) + return true; + + netId = 0; + return false; + } + + public bool TryGet(uint themeId, out PaintTheme paintTheme) + { + paintTheme = null; + + if (!hashToThemeName.TryGetValue(themeId, out string themeName)) + { + // Skin Manager might not have been updated for Multiplayer yet, or another mod may have added themes + // Try to find themes added by mods + FindModdedThemes(); + + hashToThemeName.TryGetValue(themeId, out themeName); + } + + if (themeName == null) + return false; + + return PaintTheme.TryLoad(themeName, out paintTheme); + } + + public uint RegisterTheme(PaintTheme theme) + { + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) + return 0; + + var hash = GetThemeId(theme.AssetName); + + if (hash == 0 || hash == uint.MaxValue) + return 0; + + if (hashToThemeName.TryGetValue(hash, out var existingTheme)) + { + // Check for hash collision + if (existingTheme != theme.AssetName) + { + Multiplayer.LogWarning($"Hash collision detected! Theme '{theme.AssetName}' has same hash as '{existingTheme}': {hash}."); + return 0; + } + else + { + Multiplayer.LogWarning($"Theme '{theme.AssetName}' is already registered with Id: {hash}."); + return hash; + } + } + + hashToThemeName[hash] = theme.AssetName; + themeNameToHash[theme.AssetName] = hash; + Multiplayer.Log($"PaintTheme '{theme.AssetName}' registered with netId: {hash}."); + + return hash; + } + + public void UnregisterTheme(PaintTheme theme) + { + if (theme == null || string.IsNullOrEmpty(theme.AssetName)) + return; + + var hash = GetThemeId(theme.AssetName); + + if (hashToBaseThemeName.ContainsKey(hash)) + { + Multiplayer.LogWarning($"Tried to unregister a base-game theme '{theme.AssetName}'."); + return; + } + + if (hashToThemeName.Remove(hash)) + themeNameToHash.Remove(theme.AssetName); + else + Multiplayer.LogWarning($"Tried to unregister theme '{theme.AssetName}', but theme is not registered."); + } + #endregion + + #region Helper Methods + + private uint GetThemeId(string themeName) + { + if (string.IsNullOrEmpty(themeName)) + return 0; + + return StringHashing.Fnv1aHash(themeName); + } + + private void FindModdedThemes() + { + if (moddedThemesSearched) + return; + + // Find all themes excluding base themes and register non-base themes + var themes = Object.FindObjectsOfType(); + + foreach (var theme in themes) + { + if (theme != null && + !string.IsNullOrEmpty(theme.AssetName) && + !baseThemeNamesSet.Contains(theme.AssetName)) + { + RegisterTheme(theme); + } + } + + moddedThemesSearched = true; + } + + #endregion +} diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs new file mode 100644 index 00000000..05b0d013 --- /dev/null +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -0,0 +1,684 @@ +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 readonly List messageList = new List(); + private readonly 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; + + protected 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; + } + */ + + } + + protected void OnEnable() + { + chatInputIF.onSubmit.AddListener(Submit); + chatInputIF.onValueChanged.AddListener(ChatInputChange); + + } + + protected void OnDisable() + { + chatInputIF.onSubmit.RemoveAllListeners(); + chatInputIF.onValueChanged.RemoveAllListeners(); + } + + protected void Update() + { + //Handle keypresses to open/close the chat window + if (!isOpen && Input.GetKeyDown(Multiplayer.Settings.ChatKey) && !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!"; + //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) + { + if (block) + { + denied = GameFeatureFlags.DeniedFlags; + + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Movement); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Look); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Hotbar); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.Inventory); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.ItemGrab); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.WorldInteraction); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.MouseMode); + GameFeatureFlags.Deny(GameFeatureFlags.Flag.KeyboardDriving); + + 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 ab05efeb..720b5f5c 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 8a516fa2..0e344bcf 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/NetworkedCashRegisterWithModules.cs b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs new file mode 100644 index 00000000..e127b438 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedCashRegisterWithModules.cs @@ -0,0 +1,436 @@ +using DV.CashRegister; +using DV.Interaction; +using DV.InventorySystem; +using DV.Shops; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedCashRegisterWithModules : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary cashRegisterToNetworkedCashRegister = []; + + public static bool Get(ushort netId, out NetworkedCashRegisterWithModules obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedCashRegisterWithModules)rawObj; + return b; + } + + public static bool TryGet(CashRegisterWithModules cashRegister, out NetworkedCashRegisterWithModules networkedCashRegisterWithModules) + { + return cashRegisterToNetworkedCashRegister.TryGetValue(cashRegister, out networkedCashRegisterWithModules); + } + + public static void InitialiseCashRegisters() + { + // Find all shop cash registers + var shopRegisters = GlobalShopController.Instance.globalShopList + .Select(shop => shop.cashRegister) + .ToArray(); + + //Find all CashRegistersWithModules that are placed on the map + //sort them by their hierarchy path for consistent ordering + var registers = CashRegisterBase.allCashRegisters + .OfType() + .OrderBy(r => r.transform.position.x) + .ThenBy(r => r.transform.position.y) + .ThenBy(r => r.transform.position.z) + .ToArray(); + + //Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Found: {registers?.Length}"); + + foreach (var register in registers) + { + var netRegister = register.GetOrAddComponent(); + netRegister.CashRegister = register; + netRegister.IsShopRegister = shopRegisters.Contains(register); + + if (netRegister.NetId == 0) + netRegister.Awake(); + + cashRegisterToNetworkedCashRegister[register] = netRegister; + + //Multiplayer.LogDebug(() => $"InitialiseCashRegisters() Register: {register?.GetObjectPath()}, netId: {netRegister.NetId}"); + } + } + + #endregion + + protected override bool IsIdServerAuthoritative => false; + + #region Server Variables + bool processingAction = false; + CullingManager _cullingManager; + #endregion + + #region Client Variables + public bool IsBusy => isBuying || isCancelling || isAddingCash || processingAction; + bool isBuying; + bool isCancelling; + bool isAddingCash; + public bool IsShopRegister { get; set; } = false; + + double pendingCashToAdd = 0; + #endregion + + #region Common Variables + CashRegisterWithModules CashRegister; + #endregion + + #region Unity + + protected override void Awake() + { + //Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Awake() {transform.GetObjectPath()}, {transform.position}, netId: {NetId}"); + + if (NetId == 0) + base.Awake(); + } + + protected override void OnDestroy() + { + cashRegisterToNetworkedCashRegister.Remove(CashRegister); + + if (_cullingManager != null) + _cullingManager.PlayerEnteredActivationRegion -= CullingManager_PlayerEnteredActivationRegion; + + base.OnDestroy(); + } + #endregion + + #region Server + + public void Server_InitCashRegister(CullingManager cullingManager) + { + if (!NetworkLifecycle.Instance.IsHost() || cullingManager == null) + return; + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_InitCashRegister({CashRegister.GetObjectPath()})"); + _cullingManager = cullingManager; + + if (_cullingManager != null) + _cullingManager.PlayerEnteredActivationRegion += CullingManager_PlayerEnteredActivationRegion; + } + + private void CullingManager_PlayerEnteredActivationRegion(ServerPlayer serverPlayer) + { + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.CullingManager_PlayerEnteredActivationRegion({serverPlayer.Username}) deposited cash: {CashRegister.DepositedCash}"); + if (CashRegister.DepositedCash > 0f) + { + NetworkLifecycle.Instance.Server.SendCashRegisterAction + ( + new CommonCashRegisterWithModulesActionPacket + { + NetId = NetId, + Action = CashRegisterAction.SetFunds, + Amount = CashRegister.DepositedCash + }, + [serverPlayer] + ); + } + } + + public void Server_ProcessCashRegisterAction(ServerPlayer player, CommonCashRegisterWithModulesActionPacket packet) + { + bool success = false; + CashRegisterAction response = CashRegisterAction.RejectGeneric; + + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount})"); + if (transform.PlayerCanReach(player, 1)) + { + processingAction = true; + switch (packet.Action) + { + case CashRegisterAction.Cancel: + CashRegister?.Cancel(); + success = true; + + break; + + case CashRegisterAction.Buy: + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); + + if (CashRegister.TotalUnitsInBasket() <= 0) + { + response = CashRegisterAction.RejectedNoItems; + } + else if (Inventory.Instance.PlayerMoney <= CashRegister.GetTotalCost()) + { + response = CashRegisterAction.RejectFunds; + } + else + { + success = CashRegister?.Buy() ?? false; + } + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}, {packet.Amount}) Response: {response}, Buy success: {success}, Player Money: {Inventory.Instance.PlayerMoney}, TotalCost: {CashRegister.GetTotalCost()}, TotalUnitsInBasket: {CashRegister.TotalUnitsInBasket()}"); + + break; + + case CashRegisterAction.AddCash: + + double remainingCost = CashRegister.GetRemainingCost(); + + if (remainingCost <= 0) + { + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) No remaining cost to add cash for."); + processingAction = false; + // No action needed, no response required + return; + } + else if (CashRegister.TotalUnitsInBasket() <= 0) + { + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) No items in basket to add cash for."); + response = CashRegisterAction.RejectedNoItems; + success = false; + } + else + { + double amountToAdd = Math.Min(remainingCost, Inventory.Instance.PlayerMoney); + + Inventory.Instance.RemoveMoney(amountToAdd); + CashRegister.SetCash(CashRegister.DepositedCash + amountToAdd); + + NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({packet.Action}) Added cash: {amountToAdd}, New DepositedCash: {CashRegister.DepositedCash}, Player Money: {Inventory.Instance.PlayerMoney}"); + packet.Action = CashRegisterAction.SetFunds; + packet.Amount = CashRegister.DepositedCash; + success = true; + } + + break; + + case CashRegisterAction.SetFunds: + //NetworkLifecycle.Instance.Server?.LogDebug(() => $"NetworkedCashRegisterWithModules.Server_ProcessAction({player.Username}, {packet.Action}, {packet.Amount}) Wallet: {Inventory.Instance.PlayerMoney}"); + break; + } + } + else + { + NetworkLifecycle.Instance.Server?.LogDebug(() => $"Player \"{player.Username}\" tried to interact with Cash Register , but they are too far away"); + } + + if (success) + NetworkLifecycle.Instance.Server.SendCashRegisterAction(packet, _cullingManager.ActivePlayers.ToArray()); + else + NetworkLifecycle.Instance.Server.SendCashRegisterAction + ( + new CommonCashRegisterWithModulesActionPacket + { + NetId = NetId, + Action = response, + Amount = CashRegister.DepositedCash + }, + [player] + ); + + processingAction = false; + } + + #endregion + + #region Client + + public void Client_ProcessCashRegisterAction(CashRegisterAction action, double amount) + { + NetworkLifecycle.Instance.Client?.LogDebug(() => $"NetworkedCashRegisterWithModules.Client_ProcessCashRegisterAction({action}, {amount}) isBuying: {isBuying}, isCancelling: {isCancelling}"); + switch (action) + { + case CashRegisterAction.Cancel: + + isCancelling = false; + isBuying = false; + + foreach (var module in CashRegister.registerModules) + module.ResetData(); + + CashRegister.OnUnitsToBuyChanged(); + + if (CashRegister.DepositedCash > 0) + { + CashRegister?.cancelAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); + CashRegister.DepositedCash = 0; + CashRegister?.OnDepositedUpdated(); + } + + //CashRegister?.Cancel(); + + break; + + case CashRegisterAction.Buy: + + isCancelling = false; + isBuying = false; + + CashRegister?.buyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); + + foreach (var module in CashRegister.registerModules) + module.ResetData(); + + CashRegister?.OnUnitsToBuyChanged(); + + CashRegister.DepositedCash = 0; + CashRegister?.OnDepositedUpdated(); + + CashRegister.IsProcessingTransaction = false; + + break; + + case CashRegisterAction.AddCash: + break; + + case CashRegisterAction.SetFunds: + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.Client_ProcessCashRegisterAction({action}, {amount}) Setting deposited cash."); + CashRegister?.SetCash(amount); + + break; + + case CashRegisterAction.RejectGeneric: + isBuying = false; + isCancelling = false; + + //if (isAddingCash) + //{ + //Inventory.Instance.AddMoney(pendingCashToAdd); + pendingCashToAdd = 0; + //} + + isAddingCash = false; + + break; + + case CashRegisterAction.RejectFunds: + isBuying = false; + isCancelling = false; + + //if (isAddingCash) + //{ + //Inventory.Instance.AddMoney(pendingCashToAdd); + pendingCashToAdd = 0; + //} + + isAddingCash = false; + + CashRegister?.notEnoughMoneyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); + + break; + + case CashRegisterAction.RejectedNoItems: + isBuying = false; + isCancelling = false; + + //if (isAddingCash) + //{ + //Inventory.Instance.AddMoney(pendingCashToAdd); + pendingCashToAdd = 0; + //} + + isAddingCash = false; + + foreach (var module in CashRegister.registerModules) + module.ResetData(); + + CashRegister?.OnUnitsToBuyChanged(); + + CashRegister.DepositedCash = 0; + CashRegister?.OnDepositedUpdated(); + + CashRegister?.buyAudio?.Play(CashRegister.transform.position, 1f, 1f, 0f, 1f, 500f, default, null, CashRegister.transform, false, 0f, null); + break; + } + } + + public IEnumerator Buy() + { + if (isBuying || isCancelling || NetworkLifecycle.Instance.IsProcessingPacket) + yield break; + + DisableInteraction(); + CashRegister.IsProcessingTransaction = true; + + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Buy); + + isBuying = true; + float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; + + yield return new WaitUntil(() => Time.time >= timeOut || isBuying == false); + + isBuying = false; + + CashRegister.IsProcessingTransaction = false; + EnableInteraction(); + } + + public IEnumerator Cancel() + { + if (isBuying || isCancelling || isAddingCash || NetworkLifecycle.Instance.IsProcessingPacket) + yield break; + + DisableInteraction(); + + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.Cancel); + + isCancelling = true; + float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; + + yield return new WaitUntil(() => Time.time >= timeOut || isCancelling == false); + + isCancelling = false; + + EnableInteraction(); + } + + public IEnumerator AddCash(double amount) + { + if (isBuying || isCancelling || isAddingCash || processingAction || NetworkLifecycle.Instance.IsProcessingPacket) + yield break; + + DisableInteraction(); + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.AddCash({amount}) Sending AddCash action."); + NetworkLifecycle.Instance.Client.SendCashRegisterAction(NetId, CashRegisterAction.AddCash); + + isAddingCash = true; + pendingCashToAdd = amount; + + float timeOut = Time.time + NetworkLifecycle.Instance.Client.RPC_Timeout; + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.AddCash({amount}) Waiting"); + yield return new WaitUntil(() => Time.time >= timeOut || isAddingCash == false); + + Multiplayer.LogDebug(() => $"NetworkedCashRegisterWithModules.AddCash({amount}) Wait complete, time-out: {Time.time >= timeOut}, isAddingCash: {isAddingCash}"); + + pendingCashToAdd = 0; + isAddingCash = false; + + EnableInteraction(); + } + + private void DisableInteraction() + { + CashRegister.buyButton.InteractionAllowed = false; + CashRegister.cancelButton.InteractionAllowed = false; + } + + private void EnableInteraction() + { + CashRegister.buyButton.InteractionAllowed = true; + CashRegister.cancelButton.InteractionAllowed = true; + } + + #endregion + + #region Common + + #endregion +} diff --git a/Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs b/Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs new file mode 100644 index 00000000..49185f50 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedGenericSwitch.cs @@ -0,0 +1,128 @@ +using DV.Interaction; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedGenericSwitch : MonoBehaviour +{ + #region lookup cache + private static readonly Dictionary netIdtoNetworked = []; + private static readonly Dictionary networkedToNetId = []; + private static readonly Dictionary genericSwitchToNetId = []; + + public static bool TryGet(uint netId, out NetworkedGenericSwitch netSwitch) + { + return netIdtoNetworked.TryGetValue(netId, out netSwitch); + } + + public static bool TryGetNetId(NetworkedGenericSwitch netSwitch, out uint netId) + { + return networkedToNetId.TryGetValue(netSwitch, out netId); + } + + public static bool TryGetNetId(GenericSwitch genericSwitch, out uint netId) + { + return genericSwitchToNetId.TryGetValue(genericSwitch, out netId); + } + + + #endregion + + public uint NetId { get; private set; } + public GenericSwitch Switch { get; private set; } + + + + protected void Awake() + { + + Switch = GetComponent(); + if (Switch == null) + { + Multiplayer.LogError($"NetworkedGenericSwitch.Awake() {nameof(GenericSwitch)} not found."); + return; + } + + GenerateNetId(); + + Switch.onTurnedOff.AddListener(OnSwitchValueChanged); + Switch.onTurnedOn.AddListener(OnSwitchValueChanged); + + Multiplayer.LogDebug(()=>$"NetworkedGenericSwitch.Awake() Persistence Key: \"{Switch.persistenceKey}\", netId: {NetId}"); + } + + protected void OnDestroy() + { + if (Switch != null) + { + Switch.onTurnedOff.RemoveListener(OnSwitchValueChanged); + Switch.onTurnedOn.RemoveListener(OnSwitchValueChanged); + } + + networkedToNetId.Remove(this); + netIdtoNetworked.Remove(NetId); + genericSwitchToNetId.Remove(Switch); + } + + #region server + public void Server_ReceiveSwitchState(bool isOn, ServerPlayer player) + { + if (!transform.PlayerCanReach(player)) + { + Multiplayer.LogWarning($"Player \"{player.Username}\" tried to change switch [\"{Switch.persistenceKey}\", {NetId}] state but is too far away."); + NetworkLifecycle.Instance.Server.SendGenericSwitchState(NetId, Switch.IsOn, player); + return; + } + + if (Switch.IsOn != isOn) + Switch.IsOn = isOn; + } + + #endregion + public void Client_ReceiveSwitchState(bool isOn) + { + if (Switch.IsOn != isOn) + Switch.IsOn = isOn; + } + #region client + + #endregion + + #region common + private void GenerateNetId() + { + var hash = StringHashing.Fnv1aHash(Switch.persistenceKey); + if(hash == 0 || hash == uint.MaxValue) + { + Multiplayer.LogError($"NetworkedGenericSwitch.GenerateNetId() generated invalid NetId for persistenceKey '{Switch.persistenceKey}'"); + return; + } + + NetId = hash; + + if (netIdtoNetworked.ContainsKey(hash)) + { + Multiplayer.LogError($"NetworkedGenericSwitch.GenerateNetId() generated duplicate NetId {hash} for persistenceKey '{Switch.persistenceKey}'"); + return; + } + + netIdtoNetworked[hash] = this; + networkedToNetId[this] = hash; + genericSwitchToNetId[Switch] = hash; + } + + private void OnSwitchValueChanged() + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (NetworkLifecycle.Instance.IsHost()) + NetworkLifecycle.Instance.Server.SendGenericSwitchState(NetId, Switch.IsOn); + else + NetworkLifecycle.Instance.Client.SendGenericSwitchState(NetId, Switch.IsOn); + } + #endregion +} diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs new file mode 100644 index 00000000..c12afc70 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -0,0 +1,664 @@ +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); + } + + public static bool TryGetNetId(ItemBase item, out ushort netID) + { + if (itemBaseToNetworkedItem.TryGetValue(item, out var networkedItem)) + { + netID = networkedItem.NetId; + return true; + } + + netID = 0; + return false; + } + #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.TryGet(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 00000000..aa79072a --- /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.PlayerDisconnected += 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 c4b965f9..f57d3c7a 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -1,41 +1,86 @@ +using System.Collections.Generic; using System.Linq; -using DV; namespace Multiplayer.Components.Networking.World; public class NetworkedJunction : IdMonoBehaviour { + #region Lookup Cache private static NetworkedJunction[] _indexedJunctions; - public static NetworkedJunction[] IndexedJunctions => _indexedJunctions ??= WorldData.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + private static readonly Dictionary junctionToNetworkedJunction = []; + public static NetworkedJunction[] IndexedJunctions => _indexedJunctions ??= RailTrackRegistry.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + + public static bool Get(ushort netId, out NetworkedJunction obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedJunction)rawObj; + return b; + } + + public static bool TryGet(ushort netId, out Junction junction) + { + if(Get(netId, out var networkedJunction)) + { + junction = networkedJunction.Junction; + return true; + } + + junction = null; + return false; + } + + public static bool TryGetNetId(Junction junction, out ushort netId) + { + if (junctionToNetworkedJunction.TryGetValue(junction, out var networkedJunction)) + { + netId = networkedJunction.NetId; + return true; + } + + netId = 0; + return false; + } + + #endregion protected override bool IsIdServerAuthoritative => false; public Junction Junction; + private bool initialised = false; protected override void Awake() { base.Awake(); Junction = GetComponent(); Junction.Switched += Junction_Switched; + junctionToNetworkedJunction[Junction] = this; + + initialised = NetworkLifecycle.Instance.IsHost(); + } + protected override void OnDestroy() + { + base.OnDestroy(); + + if (UnloadWatcher.isQuitting) + return; + + junctionToNetworkedJunction.Remove(Junction); } private void Junction_Switched(Junction.SwitchMode switchMode, int branch) { - if (NetworkLifecycle.Instance.IsProcessingPacket) + if (NetworkLifecycle.Instance.IsProcessingPacket || !initialised) return; + NetworkLifecycle.Instance.Client.SendJunctionSwitched(NetId, (byte)branch, switchMode); } - public void Switch(byte mode, byte selectedBranch) + public void Switch(byte mode, byte selectedBranch, bool initialising = false) { - Junction.selectedBranch = 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) - { - bool b = Get(netId, out IdMonoBehaviour rawObj); - obj = (NetworkedJunction)rawObj; - return b; + if (!initialised && initialising) + initialised = true; } } diff --git a/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs new file mode 100644 index 00000000..be6dc0a7 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedPitStopStation.cs @@ -0,0 +1,1181 @@ +using DV.CabControls; +using DV.CashRegister; +using DV.Optimizers; +using DV.ThingTypes; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.Packets.Clientbound.World; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; +using static CashRegisterModule; + +namespace Multiplayer.Components.Networking.World; + +/// +/// Handles networked interactions with pit stop stations, including vehicle selection and resource management. +/// +public class NetworkedPitStopStation : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary pitStopStationToNetworkedPitStopStation = []; + + public static bool Get(ushort netId, out NetworkedPitStopStation obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedPitStopStation)rawObj; + return b; + } + + public static void InitialisePitStops() + { + + //Find all pitstop stations that are placed on the map + //sort them by their hierarchy path for consistent ordering + var stations = Resources.FindObjectsOfTypeAll() + .Where(p => p.transform.parent != null) + .OrderBy(p => p.transform.position.x) + .ThenBy(p => p.transform.position.y) + .ThenBy(p => p.transform.position.z) + .ToArray(); + + pitStopStationToNetworkedPitStopStation.Clear(); + + //Multiplayer.LogDebug(() => $"InitialisePitStops() Found: {stations?.Length}"); + + foreach (var station in stations) + { + var netStation = station.GetOrAddComponent(); + netStation.Station = station; + + if (netStation.NetId == 0) + netStation.Awake(); + + pitStopStationToNetworkedPitStopStation[station] = netStation; + + //Multiplayer.LogDebug(() => $"InitialisePitStops() Station: {station?.GetObjectPath()}, netId: {netStation.NetId}"); + + CoroutineManager.Instance.StartCoroutine(netStation.Init()); + + } + } + #endregion + + protected override bool IsIdServerAuthoritative => false; + + const float LOADING_TIMEOUT = 5f; + const float ROTATION_SMOOTH_SPEED = 0.5f; + const float DEFAULT_DISABLER_SQR_DISTANCE = 250000f; + const float DEFAULT_DISABLER_INTERVAL = 2f; + const float NEARBY_REMOVAL_DELAY = 3f; + + #region Server variables + public CullingManager CullingManager { get; private set; } + + private readonly Dictionary resourceStartStopDelegates = []; + private readonly Dictionary resourceFlowing = []; + + private bool processingAsHost = false; + #endregion + + #region Common variables + public PitStopStation Station { get; set; } + public string StationName { get; private set; } + + private bool initialised = false; + + private CashRegisterWithModules register; + + private ResourceType[] resourceTypes = []; + + private RotaryBase carSelectorGrab; + private LeverBase faucetPositionerGrab; + private HingeJointAngleFix faucetPositioner; + private SteppedJoint faucetCrankSteppedJoint; + + private readonly Dictionary leverHandler)> leverStateLookup = []; + //private readonly Dictionary grabbedHandlerLookup = []; + private readonly Dictionary leverLookup = []; + private readonly Dictionary resourceToPluggableObject = []; + private readonly Dictionary resourceTypeToLocoResourceModule = []; + + private readonly Dictionary isResourceGrabbedDict = []; + private readonly Dictionary isResourceRemoteGrabbedDict = []; + private readonly Dictionary lastRemoteValueDict = []; + + private bool faucetTargetReached = true; + + private Coroutine faucetMoveCoroutine; + + private bool Refreshed = false; + #endregion + + #region Unity + protected override void Awake() + { + if (NetId == 0) + base.Awake(); + + StationName = $"{transform.parent.parent.name} - {transform.parent.name}"; + + if (NetworkLifecycle.Instance.IsHost()) + { + // Setup culling + var disabler = GetComponentInParent(); + + var cullingSqrDistance = DEFAULT_DISABLER_SQR_DISTANCE; + var cullingCheckInterval = DEFAULT_DISABLER_INTERVAL; + + if (disabler != null) + { + cullingSqrDistance = disabler.disableSqrDistance; + cullingCheckInterval = disabler.checkPeriodPerGO; + } + + var activationSqrDistance = cullingSqrDistance / 2; + + CullingManager = new(cullingCheckInterval, cullingSqrDistance, activationSqrDistance, NEARBY_REMOVAL_DELAY, gameObject); + CullingManager.PlayerEnteredActivationRegion += OnPlayerEnteredActivationRegion; + CullingManager.PlayerEnteredCullingRegion += OnPlayerEnteredCullingRegion; + + // Setup network events + NetworkLifecycle.Instance.OnTick += OnTick; + + NetworkLifecycle.Instance.Server.PlayerDisconnected += OnPlayerDisconnect; + + // Ensure host can interact + Refreshed = true; + } + } + + protected void OnDisable() + { + if (!NetworkLifecycle.Instance.IsHost()) + Refreshed = false; + } + + protected override void OnDestroy() + { + pitStopStationToNetworkedPitStopStation.Remove(Station); + + if (NetworkLifecycle.Instance.IsHost()) + { + foreach (var kvp in resourceStartStopDelegates) + { + var (fillStart, fillStop, drainStart, drainStop) = kvp.Value; + kvp.Key.FillStarted -= fillStart; + kvp.Key.FillStopped -= fillStop; + kvp.Key.DrainStarted -= drainStart; + kvp.Key.DrainStopped -= drainStop; + } + + resourceStartStopDelegates.Clear(); + + if (CullingManager != null) + { + CullingManager.PlayerEnteredActivationRegion -= OnPlayerEnteredActivationRegion; + CullingManager.PlayerEnteredCullingRegion -= OnPlayerEnteredCullingRegion; + CullingManager.Dispose(); + } + + NetworkLifecycle.Instance.OnTick -= OnTick; + + NetworkLifecycle.Instance.Server.PlayerDisconnected -= OnPlayerDisconnect; + + // Monitor changes to vehicles in the pit stop + Station.pitstop.CarEntered -= OnCarPitStopEntered; + } + + if (carSelectorGrab != null) + { + carSelectorGrab.Grabbed -= CarSelectorGrabbed; + carSelectorGrab.Ungrabbed -= CarSelectorUnGrabbed; + } + + if (Station?.pitstop != null) + { + Station.pitstop.CarSelected -= CarSelected; + } + + if (faucetPositionerGrab != null) + { + faucetPositionerGrab.Grabbed -= FaucetCrankGrabbed; + faucetPositionerGrab.Ungrabbed -= FaucetCrankUnGrabbed; + } + + if (faucetCrankSteppedJoint != null) + { + faucetCrankSteppedJoint.PositionChanged -= FaucetCrankPositionChanged; + } + + foreach (var kvp in leverStateLookup) + { + var (leverAmplitudeChecker, _, leverStateHandler) = kvp.Value; + leverAmplitudeChecker.RotaryStateChanged -= leverStateHandler; + } + + leverStateLookup.Clear(); + //grabbedHandlerLookup.Clear(); + leverLookup.Clear(); + base.OnDestroy(); + } + #endregion + + #region Server + + public Dictionary GetPluggables() + { + Dictionary keyValuePairs = []; + foreach (var kvp in resourceToPluggableObject) + keyValuePairs.Add(kvp.Key, kvp.Value.NetId); + + return keyValuePairs; + } + + public bool ValidateInteraction(CommonPitStopInteractionPacket packet, ServerPlayer player) + { + //todo: implement validation code (player distance, player interacting, etc.) + return true; + } + + //todo: update when merged with ModAPI branch + public void OnPlayerDisconnect(ServerPlayer player) + { + //todo: when a player disconnects, if they are interacting with a lever, cancel the interaction + //Multiplayer.LogWarning($"OnPlayerDisconnect()"); + } + + public void OnPlayerEnteredActivationRegion(ServerPlayer player) + { + // Ensure all resource data exists + InitialiseData(); + + // One struct per module type + int resourceCount = Station.locoResourceModules.resourceModules.Length; + LocoResourceModuleData[] stateData = new LocoResourceModuleData[resourceCount]; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, car count: {Station.pitstop.carList.Count}, resourceCount: {resourceCount}"); + int i; + for (i = 0; i < resourceCount; i++) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, i: {i}, data count: {Station.locoResourceModules.resourceModules[i].resourceData.Count}"); + stateData[i] = LocoResourceModuleData.From(Station.locoResourceModules.resourceModules[i]); + } + + // Car selection and lever states + int carIndex = Station.pitstop.SelectedIndex; + + PitStopPlugData[] plugData = new PitStopPlugData[resourceToPluggableObject.Count]; + + i = 0; + foreach (var plug in resourceToPluggableObject) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}, {NetId}] player: {player.Username}, plug: {plug.Key}, plug netId: {plug.Value.NetId}"); + plugData[i] = PitStopPlugData.From(plug.Value, true); + i++; + } + + int faucetPos = -1; + if (faucetCrankSteppedJoint != null) + faucetPos = faucetCrankSteppedJoint.currentNotch; + else + Multiplayer.LogWarning($"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetCrankSteppedJoint is null"); + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnPlayerEnteredActivationRegion() [{StationName}] faucetPos: {faucetPos}"); + + // Send current state + NetworkLifecycle.Instance.Server.SendPitStopBulkDataPacket(NetId, Station.pitstop.carList.Count, carIndex, faucetPos, stateData, plugData, player); + } + + public void OnPlayerEnteredCullingRegion(ServerPlayer player) + { + //todo: when a player leaves the region cancel any interactions + //Multiplayer.LogWarning($"OnPlayerDisconnect()"); + } + + public void ProcessInteractionPacketAsHost(CommonPitStopInteractionPacket packet, ServerPlayer senderPlayer) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() from: {senderPlayer.Username}, tick: {packet.Tick}, id: {senderPlayer.PlayerId}, selfpeer: {NetworkLifecycle.Instance.Server.SelfId}"); + + if (ValidateInteraction(packet, senderPlayer)) + { + // Ensure colliders for water, coal, etc. are loaded + OnCarPitStopEntered(); + + processingAsHost = true; + if (senderPlayer.PlayerId != NetworkLifecycle.Instance.Server.SelfId) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() ProcessPacketAsClient()"); + ProcessInteractionPacketAsClient(packet); + } + processingAsHost = false; + + // Send to all other players + foreach (var player in CullingManager.ActivePlayers) + { + if (player.PlayerId != senderPlayer.PlayerId) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsHost() sending to player: {player.Username}"); + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(player, packet); + } + } + } + else + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStationProcessInteractionPacketAsHost() failed validation"); + // Failed to validate, player needs to rollback interaction + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket( + senderPlayer, + new CommonPitStopInteractionPacket + { + NetId = packet.NetId, + InteractionType = PitStopStationInteractionType.Reject + } + ); + } + } + + private void OnFlowStarted(LocoResourceModule module) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnFlowStarted() {module.resourceType} [{StationName}, {NetId}]"); + resourceFlowing[module] = (isFlowing: true, wasFlowing: false, lastUpdate: false); + } + + private void OnFlowStopped(LocoResourceModule module) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnFlowStopped() {module.resourceType} [{StationName}, {NetId}]"); + + resourceFlowing[module] = (isFlowing: false, wasFlowing: true, lastUpdate: false); + SendResourceUpdate(module); + } + + private void OnTick(uint tick) + { + var modules = resourceFlowing.Keys.ToList(); + foreach (var module in modules) + { + // Ensure the final value is sent, we need perfect sync for payments to work + if (resourceFlowing[module].isFlowing || resourceFlowing[module].wasFlowing) + { + SendResourceUpdate(module); + + if (!resourceFlowing[module].isFlowing) + { + // We want one final update to ensure race conditions between flow stopping and game ticks do not cause sync issues + if (!resourceFlowing[module].lastUpdate) + resourceFlowing[module] = (isFlowing: false, wasFlowing: true, lastUpdate: true); + else + resourceFlowing[module] = (isFlowing: false, wasFlowing: false, lastUpdate: false); + } + } + } + } + + private void SendResourceUpdate(LocoResourceModule module) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) [{StationName}, {NetId}], active players: {CullingManager.ActivePlayers.Count}"); + + CommonPitStopInteractionPacket packet = new() + { + Tick = NetworkLifecycle.Instance.Tick, + NetId = NetId, + InteractionType = PitStopStationInteractionType.ResourceUpdate, + ResourceType = (int)module.resourceType, + Value = module.Data.unitsToBuy + }; + + lastRemoteValueDict[module.resourceType] = module.Data.unitsToBuy; + + foreach (var player in CullingManager.ActivePlayers) + { + if (player != null) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) [{StationName}, {NetId}], sending to peer: {player.Username}, value: {module.Data.unitsToBuy}, flowing: {module.IsFlowing}"); + NetworkLifecycle.Instance.Server.SendPitStopInteractionPacket(player, packet); + } + else + { + Multiplayer.LogWarning(() => $"NetworkedPitStopStation.SendResourceUpdate({module.resourceType}) [{StationName}, {NetId}], player is null, skipping send"); + } + } + } + + private void OnCarPitStopEntered() + { + foreach (var car in Station.pitstop.carList) + { + if (car == null) + continue; + + if (!car.AreExternalInteractablesLoaded && !car.AreDummyExternalInteractablesLoaded) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.OnCarPitStopEntered() [{StationName}, {NetId}] Loading dummy external interactables for car: {car.ID}"); + car.LoadDummyExternalInteractables(); + } + } + } + #endregion + + + #region Common + /// + /// Looks up Pluggable object by resource type + /// + public bool TryGetPluggable(ResourceType type, out NetworkedPluggableObject netPluggable) + { + return resourceToPluggableObject.TryGetValue(type, out netPluggable); + } + + /// + /// Initializes the pit stop station and sets up event handlers for grab interactions. + /// + private IEnumerator Init() + { + Multiplayer.Log($"Initialising Station {Station.GetObjectPath()}"); + + while (Station?.pitstop == null) + yield return new WaitForEndOfFrame(); + + Multiplayer.Log($"Pitstop {Station.GetObjectPath()} initialised"); + + if (NetworkLifecycle.Instance.IsHost()) + { + // Monitor changes to vehicles in the pit stop + Station.pitstop.CarEntered += OnCarPitStopEntered; + + // Ensure any cars already in the pit stop have external interactables loaded + if (Station.pitstop.carList.Count > 0) + OnCarPitStopEntered(); + } + + // Wait for cash registers to load + yield return new WaitUntil(() => transform.parent.GetComponentInChildren(true) != null); + register = transform.parent.GetComponentInChildren(true); + + if (NetworkLifecycle.Instance.IsHost()) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Waiting for NetworkedCashRegisterWithModules {StationName}"); + + NetworkedCashRegisterWithModules netRegister = null; + + yield return new WaitUntil( + () => + { + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Waiting for NetworkedCashRegisterWithModules {StationName} - spin...."); + return NetworkedCashRegisterWithModules.TryGet(register, out netRegister) && netRegister != null; + } + ); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Initialising Cash Register for station {StationName}"); + netRegister.Server_InitCashRegister(CullingManager); + } + + + //Wait for levers an knobs to load + yield return new WaitUntil(() => GetComponentInChildren(true) != null); + carSelectorGrab = GetComponentInChildren(true); + + if (carSelectorGrab != null) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); + carSelectorGrab.Grabbed += CarSelectorGrabbed; + carSelectorGrab.Ungrabbed += CarSelectorUnGrabbed; + + Station.pitstop.CarSelected += CarSelected; + } + + // Water tower positioner handle + var faucetGo = transform.parent.FindChildrenByName("FaucetCrank").FirstOrDefault(); + faucetPositionerGrab = faucetGo?.GetComponentInChildren(true); + faucetPositioner = faucetGo?.GetComponentInChildren(true); + faucetCrankSteppedJoint = faucetGo?.GetComponentInChildren(true); + + if (faucetPositionerGrab != null && faucetPositioner != null && faucetCrankSteppedJoint != null) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.Init() Grab Handler found: {carSelectorGrab != null}, Name: {carSelectorGrab.name}"); + faucetPositionerGrab.Grabbed += FaucetCrankGrabbed; + faucetPositionerGrab.Ungrabbed += FaucetCrankUnGrabbed; + faucetCrankSteppedJoint.PositionChanged += FaucetCrankPositionChanged; + } + + //build dictionaries + var resourceModules = Station?.locoResourceModules?.resourceModules; + if (resourceModules == null) + { + Multiplayer.LogWarning($"No resource modules found for station {StationName}"); + yield break; + } + + resourceTypes = resourceModules?.Select(m => m.resourceType).ToArray(); + + foreach (var resourceType in resourceTypes) + { + isResourceGrabbedDict[resourceType] = false; + isResourceRemoteGrabbedDict[resourceType] = false; + } + + StringBuilder sb = new(); + sb.AppendLine($"NetworkedPitStopStation.Init() {StationName} resources:"); + + if (resourceModules != null) + { + foreach (var resourceModule in resourceModules) + { + yield return new WaitUntil(() => resourceModule.initialized); + + resourceTypeToLocoResourceModule[resourceModule.resourceType] = resourceModule; + + //subscribe to fill/drain stop events + if (NetworkLifecycle.Instance.IsHost()) + { + void FillStartHandler() => OnFlowStarted(resourceModule); + void FillStopHandler() => OnFlowStopped(resourceModule); + void DrainStartHandler() => OnFlowStarted(resourceModule); + void DrainStopHandler() => OnFlowStopped(resourceModule); + + resourceModule.FillStarted += FillStartHandler; + resourceModule.FillStopped += FillStopHandler; + resourceModule.DrainStarted += DrainStartHandler; + resourceModule.DrainStopped += DrainStopHandler; + + resourceStartStopDelegates[resourceModule] = (FillStartHandler, FillStopHandler, DrainStartHandler, DrainStopHandler); + } + + var checker = resourceModule.GetComponentInChildren(); + var grab = resourceModule.GetComponentInChildren(); + var lever = resourceModule.GetComponentInChildren(); + if (checker != null && grab != null) + { + + //Delegates for handlers + void LeverStatehandler(int state) => OnLeverPositionChange(resourceModule, state); + + //Subscribe + checker.RotaryStateChanged += LeverStatehandler; + + //Store delegate + leverStateLookup[resourceModule.resourceType] = (checker, resourceModule, LeverStatehandler); + //grabbedHandlerLookup[resourceModule.resourceType] = grab; + leverLookup[resourceModule.resourceType] = lever; + + //sb.AppendLine($"\t{resourceModule.resourceType}, Grab Handler found: {grab != null}, Name: {grab.name}"); + sb.AppendLine($"\t{resourceModule.resourceType}, Rotary Amplitude Handler found: {checker != null}, Name: {checker.name}"); + } + else + { + sb.AppendLine($"\t{resourceModule.resourceType}, Failed to find component. Grab Handler found: {grab != null}, Amplitude Checker found: {checker != null}"); + } + + var plug = resourceModule.resourceHose; + if (plug != null) + { + var netPlug = plug.GetOrAddComponent(); + resourceToPluggableObject[resourceModule.resourceType] = netPlug; + netPlug.InitPitStop(this); + } + } + } + else + { + sb.AppendLine($"ERROR Station is Null {Station == null}, resource modules: {Station?.locoResourceModules}"); + } + + Multiplayer.LogDebug(() => sb.ToString()); + + initialised = true; + } + + private IEnumerator SetUnitsDelayed(LocoResourceModule rm) + { + if (rm == null || !isResourceRemoteGrabbedDict.ContainsKey(rm.resourceType)) + yield break; + + var resourceType = rm.resourceType; + + yield return new WaitUntil(() => !isResourceRemoteGrabbedDict[resourceType] && !rm.IsFlowing); + yield return null; + + SetUnits(rm, lastRemoteValueDict[resourceType]); + } + + private void SetUnits(LocoResourceModule rm, float units) + { + if (rm == null) + return; + + float clamped = Mathf.Clamp(units, rm.AbsoluteMinValue, rm.AbsoluteMaxValue); + + lastRemoteValueDict[rm.resourceType] = clamped; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SetUnits({rm.resourceType}, {units}) clamped: {clamped}, flowMultiplier: {rm.flowMultiplier}, flowRate: {rm.flowRate}, isFlowing: {rm.IsFlowing}"); + rm.SetUnitsToBuy(clamped); + } + + /// + /// Initialises data elements for each car in each resource module + /// + private void InitialiseData() + { + foreach (var resourceModule in Station.locoResourceModules.resourceModules) + { + if (resourceModule == null) + continue; + + // Make sure resourceData has enough entries for all cars + while (resourceModule.resourceData.Count < Station.pitstop.carList.Count) + { + if (resourceModule.resourceData.Count > 0) + resourceModule.resourceData.Add(new CashRegisterModuleData(resourceModule.resourceData[0])); + else + resourceModule.resourceData.Add(new CashRegisterModuleData()); + } + } + } + + /// + /// Sets the rotation of the faucet handle to the specified percentage + /// + public void SetFaucetRotation(int notch) + { + if (faucetPositioner == null) + return; + + if (faucetMoveCoroutine != null) + StopCoroutine(faucetMoveCoroutine); + + faucetTargetReached = false; + + faucetMoveCoroutine = StartCoroutine(SmoothMoveToNotch(notch)); + } + + private IEnumerator SmoothMoveToNotch(int targetNotch) + { + float min = faucetCrankSteppedJoint.joint.limits.min; + float max = faucetCrankSteppedJoint.joint.limits.max; + + float startAngle = faucetCrankSteppedJoint.jointAngleFix.Angle; + float endAngle = faucetCrankSteppedJoint.AngleForNotch(targetNotch); + float elapsed = 0f; + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, startAngle: {startAngle}, endAngle: {endAngle}"); + + targetNotch = Mathf.Clamp(targetNotch, 0, faucetCrankSteppedJoint.notches - 1); + + while (faucetCrankSteppedJoint.currentNotch != targetNotch && elapsed < 2f) + { + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, currentNotch: {faucetCrankSteppedJoint.currentNotch}"); + elapsed += Time.deltaTime; + float t = Mathf.Clamp01(elapsed / ROTATION_SMOOTH_SPEED); + float newAngle = Mathf.Lerp(startAngle, endAngle, t); + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, startAngle: {startAngle}, endAngle: {endAngle}, newAngleUnclamped: {newAngle}"); + + newAngle = Mathf.Clamp(newAngle, min, max); + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() targetNotch: {targetNotch}, startAngle: {startAngle}, endAngle: {endAngle}, newAngleClamped: {newAngle}"); + + var spring = faucetCrankSteppedJoint.joint.spring; + spring.targetPosition = newAngle; + faucetCrankSteppedJoint.joint.spring = spring; + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch()targetNotch: {targetNotch}, newAngle: {newAngle}, t: {t}, elapsed: {elapsed}"); + + yield return null; + } + + yield return null; + + faucetTargetReached = true; + + //Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SmoothMoveToNotch() Finished moving to notch: {targetNotch}, final angle: {faucetCrankSteppedJoint.jointAngleFix.Angle}"); + } + + + /// + /// Set the car selection index + /// + public void SetCarSelection(int selection) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.SetCarSelection({selection}) [{StationName}, {NetId}] car count: {Station.pitstop.carList.Count}"); + if (selection >= 0 && selection < Station.pitstop.carList.Count) + { + Station.pitstop.currentCarIndex = selection; + Station.pitstop.OnCarSelectionChanged(); + } + else + { + Multiplayer.LogWarning($"Pit Stop car selection change out of bounds! Selected: {selection}, current car count: {Station.pitstop.carList.Count}"); + } + } + #endregion + + + #region Client + /// + /// Handles grab interactions for the car selector knob. + /// + private void CarSelectorGrabbed(ControlImplBase _) + { + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"CarSelectorGrabbed() {StationName}"); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.CarSelectorGrab, null, 0); + } + + /// + /// Handles end of grab (release) interactions for the car selector knob. + /// + private void CarSelectorUnGrabbed(ControlImplBase _) + { + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"CarSelectorUnGrabbed() {StationName}"); + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.CarSelectorUngrab, null, Station.pitstop.SelectedIndex); + } + + /// + /// Handles change of selected car events. + /// + private void CarSelected() + { + Multiplayer.LogDebug(() => $"CarSelected() [{StationName}, {NetId}] selected: {Station.pitstop.SelectedIndex}"); + + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + // Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"CarSelected() selected: {Station.pitstop.SelectedIndex}"); + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.CarSelection, null, Station.pitstop.SelectedIndex); + } + + /// + /// Handles grab interactions for resource module levers. + /// + /// The resource module being grabbed. + private void OnLeverPositionChange(LocoResourceModule module, int state) + { + // Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"OnLeverPositionChange() {StationName}, module: {module.resourceType}, state: {state}"); + + if (state == 0) + { + //lever returned home + isResourceGrabbedDict[module.resourceType] = false; + } + else + { + isResourceGrabbedDict[module.resourceType] = true; + } + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.LeverState, module.resourceType, state); + } + + /// + /// Handles grab interactions for the faucet positioning handle (water towers). + /// + private void FaucetCrankGrabbed(ControlImplBase _) + { + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"FaucetCrankGrabbed() {StationName}"); + + int notch = -1; + if (faucetCrankSteppedJoint != null) + { + notch = faucetCrankSteppedJoint.currentNotch; + } + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetGrab, null, notch); + } + + /// + /// Handles end of grab (release) interactions for the faucet positioning handle (water towers). + /// + private void FaucetCrankUnGrabbed(ControlImplBase _) + { + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"FaucetCrankUnGrabbed() {StationName}, percentage: {faucetPositioner.Percentage}"); + + int notch = -1; + if (faucetCrankSteppedJoint != null) + notch = faucetCrankSteppedJoint.currentNotch; + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetUngrab, null, notch); + } + + /// + /// Handles non-grab changes to the faucet positioning handle (water towers), e.g. scrolling. + /// + private void FaucetCrankPositionChanged(ValueChangedEventArgs args) + { + Multiplayer.LogDebug(() => $"FaucetCrankPositionChanged() {StationName}, oldValue: {args.oldValue}, newValue: {args.newValue}, delta: {args.delta}"); + + if (NetworkLifecycle.Instance.IsProcessingPacket || (NetworkLifecycle.Instance.IsHost() && processingAsHost)) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + if (!faucetTargetReached) + { + Multiplayer.LogDebug(() => $"FaucetCrankPositionChanged() {StationName} faucet target not reached, ignoring position change"); + return; + } + + int notch = -1; + if (faucetCrankSteppedJoint != null) + { + notch = faucetCrankSteppedJoint.currentNotch; + } + + NetworkLifecycle.Instance?.Client.SendPitStopInteractionPacket(NetId, PitStopStationInteractionType.FaucetPosition, null, notch); + } + + public void ProcessBulkUpdate(ClientboundPitStopBulkUpdatePacket packet) + { + // Packet is broken up due to SubscribeResusable reusing/overwriting packet data + CoroutineManager.Instance.StartCoroutine(ProcessBulkUpdate_Internal(packet.CarCount, packet.CarSelection, packet.FaucetNotch, packet.ResourceData, packet.PlugData)); + } + + private IEnumerator ProcessBulkUpdate_Internal(int carCount, int carSelection, int faucetNotch, LocoResourceModuleData[] resourceData, PitStopPlugData[] plugData) + { + float time = Time.time; + + // Allow pit stop to complete loading and cars to load in the pit stop + Multiplayer.Log($"Processing bulk update for [{StationName}, {NetId}]"); + + yield return new WaitUntil + ( + () => + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Initialised: {initialised}, Active:{Station?.gameObject?.activeInHierarchy}, Packet Car Count: {carCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {carCount == Station.pitstop?.carList?.Count}, time elapsed: {(Time.time - time)}"); + + if (Station?.gameObject?.activeInHierarchy == false) + { + // Don't time out if we're waiting for the object to be enabled + time = Time.time; + return false; + } + + // Try to trigger colliders manually + if (initialised && Station?.pitstop?.carList != null && carCount != Station.pitstop.carList.Count) + Station?.pitstop?.RefreshPitStopCarPresence(); + + return (initialised && Station?.pitstop?.carList != null && carCount == Station.pitstop.carList.Count) + || (Time.time - time) > LOADING_TIMEOUT; + + } + ); + + + yield return new WaitForEndOfFrame(); + yield return new WaitForEndOfFrame(); + + if ((Time.time - time) > LOADING_TIMEOUT) + Multiplayer.LogWarning($"PitStop [{StationName}] timed out waiting for load. PitStop Initialised: {initialised}, Packet Car Count: {carCount}, Station Car Count: {Station.pitstop?.carList?.Count}, Car Count Matched: {carCount == Station.pitstop?.carList?.Count}"); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}] Car count: {carCount}, resource data count: {resourceData.Count()}, resource data: [{string.Join(", ", resourceData.Select(x => $"{x.ResourceType}: {{{string.Join(", ", x.Values)}}}"))}]"); + // Make sure the data elements exist prior to attempting to load them + InitialiseData(); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}] PitStop bulk data car count matches. Station module count: {Station?.locoResourceModules?.resourceModules?.Count()}, Packet resource count: {resourceData?.Count()}"); + + // Load the data for each car and resource module + foreach (var resource in resourceData) + { + if (!resourceTypeToLocoResourceModule.TryGetValue(resource.ResourceType, out var module)) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Failed to find resource module for type {resource.ResourceType}"); + continue; + } + + if (module != null) + { + if (module.resourceData.Count == resource.Values.Length) + { + for (int i = 0; i < module.resourceData.Count; i++) + { + module.resourceData[i].unitsToBuy = resource.Values[i]; + } + } + else + { + Multiplayer.LogWarning($"PitStop bulk data count mismatch post-force: {module.resourceData.Count} != {resource.Values.Count()}"); + } + } + else + Multiplayer.LogWarning($"PitStop module not found for resource type: {resource.ResourceType}"); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Resource module data loaded for {resource.ResourceType}"); + + // Set the grab state + bool grabbed = (resource.FillingState != LocoResourceModuleFillingState.None); + bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resource.ResourceType, out var localGrabbed) && localGrabbed; + + leverLookup.TryGetValue(resource.ResourceType, out LeverBase lever); + //grabbedHandlerLookup.TryGetValue(resource.ResourceType, out LeverBase grab); + + if (!isLocallyGrabbed) + { + lever?.BlockControl(grabbed); + if (lever != null) + lever.InteractionAllowed = !grabbed; + + if (grabbed) + lever?.ForceEndInteraction(); + } + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Grab states set for {resource.ResourceType}, state: {grabbed}"); + + int valvePos = resource.FillingState switch + { + LocoResourceModuleFillingState.Filling => -1, + LocoResourceModuleFillingState.Draining => 1, + _ => 0 + }; + + module.OnValvePositionChange(valvePos); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Valve position set for {resource.ResourceType}, position: {valvePos}"); + + // Update remote grab state + isResourceRemoteGrabbedDict[resource.ResourceType] = grabbed; + } + + // Refresh the cash register display + register?.OnUnitsToBuyChanged(); + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Car Index: {carSelection}"); + SetCarSelection(carSelection); + + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] bulk data Plugs {plugData.Count()}"); + + // Sync plugs + foreach (var plug in plugData) + { + var result = NetworkedPluggableObject.Get(plug.NetId, out var netPlug); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Plugs netId: {plug.NetId}, found: {result}"); + + netPlug?.ProcessBulkUpdate(plug); + } + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Plugs synced"); + + // Sync faucet position + if (faucetPositioner != null) + { + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Faucet notch: {faucetNotch}"); + + SetFaucetRotation(faucetNotch); + + while (!faucetTargetReached) + yield return null; + } + + // Mark data as refreshed to allow player interactions + Refreshed = true; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessBulkUpdate_Internal() [{StationName}, {NetId}] Bulk data refreshed"); + } + + /// + /// Processes incoming network packets for pit stop interactions. + /// + /// The packet containing interaction data. + public void ProcessInteractionPacketAsClient(CommonPitStopInteractionPacket packet) + { + LeverBase grab = null; + RotaryAmplitudeChecker amplitudeChecker = null; + LeverBase lever = null; + LocoResourceModule resourceModule = null; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessInteractionPacketAsClient() [{StationName}, {NetId}] Tick: {packet.Tick}, Packet InteractionType: {packet.InteractionType}, ResourceType: {packet.ResourceType}, Value: {packet.Value}"); + + // Validate interaction type + if (!Enum.IsDefined(typeof(PitStopStationInteractionType), packet.InteractionType)) + { + Multiplayer.LogWarning($"Invalid interaction type: {packet.InteractionType} in ProcessInteractionPacketAsClient()"); + return; + } + + PitStopStationInteractionType interactionType = (PitStopStationInteractionType)packet.InteractionType; + + bool isResourceSelection = interactionType switch + { + PitStopStationInteractionType.CarSelectorGrab => false, + PitStopStationInteractionType.CarSelectorUngrab => false, + PitStopStationInteractionType.CarSelection => false, + + PitStopStationInteractionType.FaucetGrab => false, + PitStopStationInteractionType.FaucetUngrab => false, + PitStopStationInteractionType.FaucetPosition => false, + + _ => true, + }; + + // Validate resource type (no resource type for car selectors + if (isResourceSelection && !Enum.IsDefined(typeof(ResourceType), packet.ResourceType)) + { + Multiplayer.LogWarning($"Received invalid ResourceType \"{packet.ResourceType}\" at Pit Stop station {StationName}"); + return; + } + + ResourceType resourceType = (ResourceType)packet.ResourceType; + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}"); + + // Validate resource module exists + if (isResourceSelection && !resourceTypeToLocoResourceModule.TryGetValue(resourceType, out resourceModule)) + { + Multiplayer.LogWarning($"Could not find LocoResourceModule for ResourceType \"{resourceType}\" at Pit Stop station {StationName}"); + return; + } + + switch (interactionType) + { + case PitStopStationInteractionType.Reject: + //todo: implement rejection + break; + + case PitStopStationInteractionType.LeverState: + leverLookup.TryGetValue(resourceType, out lever); + + if (!leverStateLookup.TryGetValue(resourceType, out var tup)) + { + Multiplayer.LogError($"Could not find Rotary Amplitude Handler in rotaryAmplitudeLookup for Pit Stop station {StationName}, resource type: {resourceType}"); + return; + } + else + { + (amplitudeChecker, resourceModule, _) = tup; + + if (packet.Value < RotaryAmplitudeChecker.MIN_REACHED || packet.Value > RotaryAmplitudeChecker.MAX_REACHED) + { + Multiplayer.LogError($"Invalid lever value ({packet.Value}) received for Pit Stop station {StationName}, resource type: {resourceType}"); + return; + } + } + + bool grabbed = (packet.Value != 0); + bool isLocallyGrabbed = isResourceGrabbedDict.TryGetValue(resourceType, out var localGrabbed) && localGrabbed; + + if (!isLocallyGrabbed) + { + lever?.BlockControl(grabbed); + if (lever != null) + lever.InteractionAllowed = !grabbed; + + if (grabbed) + grab?.ForceEndInteraction(); + } + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, grabbed: {grabbed}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}"); + + resourceModule.OnValvePositionChange((int)packet.Value); + + // Update remote grab state and delay set units + bool wasRemoteGrabbed = isResourceRemoteGrabbedDict[resourceType]; + isResourceRemoteGrabbedDict[resourceType] = grabbed; + + if (wasRemoteGrabbed && !grabbed) + { + CoroutineManager.Instance.StartCoroutine(SetUnitsDelayed(resourceModule)); + } + break; + + case PitStopStationInteractionType.ResourceUpdate: + + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, resourceModule: {resourceModule != null}, isResourceRemoteGrabbed: {isResourceRemoteGrabbedDict[resourceType]}"); + + // Validate the value range + if (packet.Value < resourceModule.AbsoluteMinValue || packet.Value > resourceModule.AbsoluteMaxValue) + { + Multiplayer.LogError($"Invalid Pit Stop state value: {packet.Value} for resource {resourceModule.resourceType}"); + return; + } + + SetUnits(resourceModule, packet.Value); + Multiplayer.LogDebug(() => $"NetworkedPitStopStation.ProcessPacket() [{StationName}, {NetId}] {interactionType}, resource type: {resourceType}, state: {packet.Value}, flowing: {resourceModule.IsFlowing}"); + + break; + + case PitStopStationInteractionType.CarSelectorGrab: + //block interaction + carSelectorGrab?.BlockControl(true); + if (carSelectorGrab != null) + carSelectorGrab.InteractionAllowed = false; + break; + + case PitStopStationInteractionType.CarSelectorUngrab: + //allow interaction + carSelectorGrab?.BlockControl(false); + if (carSelectorGrab != null) + carSelectorGrab.InteractionAllowed = true; + SetCarSelection((int)packet.Value); + break; + + case PitStopStationInteractionType.CarSelection: + SetCarSelection((int)packet.Value); + break; + + case PitStopStationInteractionType.FaucetGrab: + //block interaction + faucetPositionerGrab?.BlockControl(true); + if (faucetPositionerGrab != null) + faucetPositionerGrab.InteractionAllowed = false; + break; + + case PitStopStationInteractionType.FaucetUngrab: + //allow interaction + faucetPositionerGrab?.BlockControl(false); + if (faucetPositionerGrab != null) + faucetPositionerGrab.InteractionAllowed = true; + + SetFaucetRotation((int)packet.Value); + + break; + + case PitStopStationInteractionType.FaucetPosition: + + SetFaucetRotation((int)packet.Value); + + break; + } + } + #endregion +} diff --git a/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs new file mode 100644 index 00000000..0ccef08f --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedPluggableObject.cs @@ -0,0 +1,787 @@ +using DV.CabControls; +using DV.Interaction; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedPluggableObject : IdMonoBehaviour +{ + private const float DISTANCE_TOLERANCE = 1.05f; //allow 5% tolerance for interactions coming from clients + private const float GRAB_SQR_DISTANCE = GrabberRaycaster.SPHERE_CAST_MAX_DIST * GrabberRaycaster.SPHERE_CAST_MAX_DIST * DISTANCE_TOLERANCE; + private const float DOCK_SQR_DISTANCE = 2f * 2f * DISTANCE_TOLERANCE; //no accessible constant available, hardcoded in to `PluggableObject.ScanForHit()` + + private const sbyte INVALID_SOCKET = -1; + private const ushort INVALID_NETID = 0; + + #region Lookup Cache + private static readonly Dictionary plugToStation = []; + private static readonly Dictionary plugToNetworkedPluggable = []; + + public static bool Get(ushort netId, out NetworkedPluggableObject obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedPluggableObject)rawObj; + return b; + } + + public static bool Get(PluggableObject pluggableObject, out NetworkedPluggableObject obj) + { + bool b = plugToNetworkedPluggable.TryGetValue(pluggableObject, out obj); + return b; + } + #endregion + + protected override bool IsIdServerAuthoritative => false; + + #region Server Variables + public ServerPlayer HeldBy { get; private set; } + #endregion + + #region Common Variables + public PluggableObject PluggableObject { get; private set; } + public Rigidbody PlugRB { get; private set; } + public PropHose Hose { get; private set; } + public NetworkedPitStopStation Station { get; private set; } + public bool IsConnecting { get; set; } = false; + + public bool IsHeld => playerHolding != 0 || HeldBy != null || PluggableObject.controlGrabbed; + + private CustomNonVrGrabAnchor nonVrGrabAnchor; + + private bool handlersInitialised = false; + + public ushort TrainCarNetId { get; private set; } = INVALID_NETID; //initialise to invalid TrainCar + public sbyte SocketIndex { get; private set; } = INVALID_SOCKET; //initialise to invalid socket + + private PlugInteractionType currentInteraction = PlugInteractionType.Rejected; + + private bool processingAsHost = false; + #endregion + + #region Client Variables + private bool Refreshed = false; + private byte playerHolding; + #endregion + + #region Unity + protected override void Awake() + { + if (NetId == INVALID_NETID) + base.Awake(); + + PluggableObject = GetComponent(); + Hose = transform.parent.GetComponentInChildren(); + PlugRB = PluggableObject.GetComponent(); + + + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Awake() {this.GetObjectPath()}, netId: {NetId}, PluggableObject found: {PluggableObject != null}, RB Found: {PlugRB != null}, Hose found: {Hose != null}"); + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.PlayerDisconnected += OnPlayerDisconnected; + + Refreshed = true; + } + } + + protected IEnumerator Start() + { + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + yield return new WaitUntil(() => PluggableObject?.controlBase != null); + + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.Start() Controlbase {PluggableObject?.controlBase?.spec?.name}, {transform.parent.name}"); + + nonVrGrabAnchor = this.GetComponent(); + + PluggableObject.controlBase.Grabbed += OnGrabbed; + PluggableObject.controlBase.Ungrabbed += OnUngrabbed; + PluggableObject.PluggedIn += OnPluggedIn; + + handlersInitialised = true; + } + + protected void OnDisable() + { + if (!NetworkLifecycle.Instance.IsHost()) + Refreshed = false; + } + + protected override void OnDestroy() + { + if (UnloadWatcher.isUnloading) + { + plugToStation.Clear(); + plugToNetworkedPluggable.Clear(); + } + else + { + plugToStation.Remove(this); + plugToNetworkedPluggable.Remove(PluggableObject); + } + + if (PluggableObject?.controlBase != null && handlersInitialised) + { + PluggableObject.controlBase.Grabbed -= OnGrabbed; + PluggableObject.controlBase.Ungrabbed -= OnUngrabbed; + PluggableObject.PluggedIn -= OnPluggedIn; + } + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.PlayerDisconnected -= OnPlayerDisconnected; + } + + base.OnDestroy(); + } + + protected void LateUpdate() + { + if (currentInteraction == PlugInteractionType.Rejected) + return; + + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.LateUpdate()station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + if (!processingAsHost) + { + NetworkLifecycle.Instance.Client?.SendPitStopPlugInteractionPacket(NetId, currentInteraction, transform.position - WorldMover.currentMove, transform.rotation, TrainCarNetId, SocketIndex); + } + else + { + //this will only trigger when there's a valid state to be sent (current interaction is not rejected) + //and the host is processing a packet + //this should be the end of the processing, even for docking plugs, so we can clear the connecting and processing flags + IsConnecting = false; + processingAsHost = false; + } + + currentInteraction = PlugInteractionType.Rejected; + } + #endregion + + #region Server + + public void ProcessInteractionPacketAsHost(CommonPitStopPlugInteractionPacket packet, ServerPlayer senderPlayer) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() NetId: {NetId}, InteractionType: {packet.InteractionType}, from player: {senderPlayer.Username}"); + + if (ValidateInteraction(packet, senderPlayer)) + { + //passed validation, set server states + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() VALIDATION PASSED for NetId: {NetId}"); + + switch (packet.InteractionType) + { + case PlugInteractionType.PickedUp: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = senderPlayer; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.Dropped: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.Yanked: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + //we should never reach this as Yanked is only sent by the server and should be rejected by validation + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.DockHome: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + break; + + case PlugInteractionType.DockSocket: + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Processing {packet.InteractionType} for NetId: {NetId}"); + HeldBy = null; + TrainCarNetId = packet.TrainCarNetId; + SocketIndex = packet.SocketIndex; + + break; + } + + packet.PlayerId = senderPlayer.PlayerId; + + //Allow host to process packet if not from a local client + if (!NetworkLifecycle.Instance.IsClientRunning || (NetworkLifecycle.Instance.IsClientRunning && senderPlayer.PlayerId != NetworkLifecycle.Instance.Server.SelfId)) + { + processingAsHost = true; + ProcessPacket(packet); + } + + //send to all players in active area, except originator + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + foreach (var player in Station.CullingManager.ActivePlayers) + { + if (player.PlayerId != senderPlayer.PlayerId) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteractionPacketAsHost() Sending interaction packet to player: {player.Username}"); + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + } + + } + else + { + //Failed to validate, player needs to rollback interaction + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(senderPlayer, new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.Rejected, + }); + } + } + + public bool ValidateInteraction(CommonPitStopPlugInteractionPacket packet, ServerPlayer player) + { + PlugInteractionType interactionType = packet.InteractionType; + + if (interactionType == PlugInteractionType.Rejected || interactionType == PlugInteractionType.Yanked) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} sent an invalid interaction type ({interactionType})!"); + return false; + } + + //validate ownership of object + if (HeldBy == null && interactionType != PlugInteractionType.PickedUp) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to interact with a plug that they are not holding!"); + return false; + } + + //ensure the player is holding the object or no one is holding the object + if (HeldBy != null && HeldBy != player) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to interact with a plug that is held by {HeldBy.Username}"); + return false; + } + + if (interactionType == PlugInteractionType.DockSocket) + { + + //verify TrainCar + if (packet.TrainCarNetId == 0 || !NetworkedTrainCar.TryGet(packet.TrainCarNetId, out NetworkedTrainCar networkedTrainCar) || networkedTrainCar == null) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, trainCarNetId: {packet.TrainCarNetId}, NetworkedTrainCar not found!"); + return false; + } + + //verify TrainCar is in station + if (!(Station?.Station?.pitstop?.carList?.Contains(networkedTrainCar.TrainCar) ?? false)) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, trainCarNetId: {packet.TrainCarNetId}, Not in Pitstop car list!"); + return false; + } + + //verify socket exists (only locos have sockets) + var socket = GetTrainCarSocket(networkedTrainCar, packet.SocketIndex); + if (socket == null) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to insert plug that into a socket that doesn't exist!"); + return false; + } + + //verify socket is compatible + if (!socket.CanAccept(PluggableObject) && socket.Plug != PluggableObject) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to dock a {PluggableObject.connectionTag} plug into a {socket.connectionTag} socket, but socket is not compatible!"); + return false; + } + + //verify distance to socket + float sqrDistance = (socket.transform.GetWorldAbsolutePosition() - PluggableObject.transform.GetWorldAbsolutePosition()).sqrMagnitude; + if (sqrDistance > DOCK_SQR_DISTANCE) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to dock a plug into {networkedTrainCar.TrainCar.ID}, but socket is too far away!"); + return false; + } + } + else + { + if (interactionType == PlugInteractionType.DockHome) + { + //verify distance to socket + var socket = PluggableObject.startAttachedTo; + float sqrDistance = (socket.transform.GetWorldAbsolutePosition() - PluggableObject.transform.GetWorldAbsolutePosition()).sqrMagnitude; + if (sqrDistance > DOCK_SQR_DISTANCE) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to dock a plug into the stand, but socket is too far away!"); + return false; + } + } + else if (interactionType == PlugInteractionType.Dropped) + { + // no verifications required + } + else if (interactionType == PlugInteractionType.PickedUp) + { + float sqrDistance = (player.AbsoluteWorldPosition - PluggableObject.transform.GetWorldAbsolutePosition()).sqrMagnitude; + + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ValidateInteraction() NetId: {NetId}, {interactionType}, player pos: {player.AbsoluteWorldPosition}, plug pos: {PluggableObject.transform.GetWorldAbsolutePosition()}, sqrDistance: {sqrDistance}, Raycast distance: {GRAB_SQR_DISTANCE}"); + if (sqrDistance > GRAB_SQR_DISTANCE) + { + NetworkLifecycle.Instance.Server.LogWarning($"{player.Username} attempted to interact with a plug that is too far away!"); + return false; + } + } + } + + return true; + } + + public void YankedByRope(Vector3 force, ForceMode mode) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.YankedByRope() [{transform.parent.name}, {NetId}] station: {Station?.StationName}, force: {force}"); + + //cancel any client events + currentInteraction = PlugInteractionType.Rejected; + + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + IsConnecting = false; + + var packet = new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.Yanked, + Position = transform.position - WorldMover.currentMove, + Rotation = transform.rotation, + YankForce = force, + YankMode = mode, + }; + + //Allow host to process packet + processingAsHost = true; + ProcessPacket(packet); + processingAsHost = false; + + //send to all players in active area, except originator and self client + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + foreach (var player in Station.CullingManager.ActivePlayers) + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + + public void SnappedByRope() + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.SnappedByRope() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + + //cancel any client events + currentInteraction = PlugInteractionType.Rejected; + + HeldBy = null; + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + IsConnecting = false; + + var packet = new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.DockHome, + }; + + //Allow host to process packet + processingAsHost = true; + ProcessPacket(packet); + processingAsHost = false; + + //send to all players in active area, except originator and self client + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + foreach (var player in Station.CullingManager.ActivePlayers) + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + + private void OnPlayerDisconnected(ServerPlayer disconnectedPlayer) + { + if (HeldBy == null || HeldBy != disconnectedPlayer) + return; + + HeldBy = null; + DropPlug(); + + if (Station == null || Station.CullingManager == null || Station.CullingManager.ActivePlayers.Count == 0) + return; + + //cache packet + var packet = new CommonPitStopPlugInteractionPacket + { + NetId = NetId, + InteractionType = PlugInteractionType.Dropped, + }; + + foreach (var player in Station.CullingManager.ActivePlayers) + { + if (player != disconnectedPlayer && player.PlayerId != NetworkLifecycle.Instance.Server.SelfId) + NetworkLifecycle.Instance.Server.SendPitStopPlugInteractionPacket(player, packet); + } + } + + #endregion + + #region Common + + public void ProcessPacket(CommonPitStopPlugInteractionPacket packet) + { + ProcessInteraction(packet.InteractionType, packet.PlayerId, packet.TrainCarNetId, packet.SocketIndex, packet.Position, packet.Rotation, packet.YankForce, packet.YankMode); + } + + public void ProcessBulkUpdate(PitStopPlugData data) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessBulkUpdate() netId: {NetId}"); + CoroutineManager.Instance.StartCoroutine(WaitForInit(data)); + } + + private IEnumerator WaitForInit(PitStopPlugData data) + { + yield return new WaitUntil(() => PluggableObject != null && PluggableObject.initialized); + + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.WaitForInit() netId: {NetId} Complete"); + + var interaction = data.State; + ProcessInteraction(interaction, data.PlayerId, data.TrainCarNetId, data.SocketIndex, data.Position, data.Rotation); + + //wait 1 frame for plugs that are docking + yield return null; + //clear the docking flag + if (interaction == PlugInteractionType.DockSocket || interaction == PlugInteractionType.DockHome) + IsConnecting = false; + + //allow the player to interact + Refreshed = true; + } + + public void ProcessInteraction(PlugInteractionType interaction, byte playerId, ushort trainNetId, sbyte socketIndex, Vector3? newPosition, Quaternion? newRotation, Vector3? yankForce = null, ForceMode yankMode = ForceMode.Impulse) + { + bool result; + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.ProcessInteraction({interaction}, {playerId}, {trainNetId}, {socketIndex}, {newPosition?.ToString()}, {newRotation?.ToString()}, {yankForce}, {yankMode}) netId: {NetId}"); + + switch (interaction) + { + case PlugInteractionType.Rejected: + //todo implement rejection + break; + + case PlugInteractionType.PickedUp: + //Handle the picked up state + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Picked Up, player: {playerHolding}"); + + GrabPlug(playerId); + + break; + + case PlugInteractionType.Dropped: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Dropped"); + + DropPlug(); + + if (newPosition == null || newRotation == null) + return; + + transform.position = (Vector3)newPosition + WorldMover.currentMove; + transform.rotation = (Quaternion)newRotation; + + break; + + case PlugInteractionType.Yanked: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, Yanked"); + + DropPlug(); + + if (newPosition != null || newRotation != null) + { + transform.position = (Vector3)newPosition + WorldMover.currentMove; + transform.rotation = (Quaternion)newRotation; + } + + PlugRB?.AddForce((Vector3)yankForce, yankMode); + + CoroutineManager.Instance.StartCoroutine(WaitForYankSettle()); + + break; + + case PlugInteractionType.DockHome: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome"); + + DropPlug(); + + //result = PluggableObject.InstantSnapTo(PluggableObject.startAttachedTo); + result = PluggableObject.StartSnappingTo(PluggableObject.startAttachedTo, true); + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockHome, result: {result}"); + break; + + case PlugInteractionType.DockSocket: + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {socketIndex}"); + + DockTrainCar(trainNetId, socketIndex); + break; + } + } + + private void BlockInteraction(bool block) + { + Multiplayer.LogDebug(() => $"BlockInteraction({block})"); + if (block) + { + PluggableObject.DisableStandaloneComponents(); + PluggableObject.DisableColliders(); + } + else + { + PluggableObject.EnableStandaloneComponents(); + PluggableObject.EnableColliders(); + } + } + + public void InitPitStop(NetworkedPitStopStation netPitStop) + { + if (NetId == 0) + base.Awake(); + + if (plugToStation.TryGetValue(this, out _)) + { + Multiplayer.LogWarning($"Lookup cache 'plugToStation' already contains NetworkedPitStopStation \"{netPitStop?.StationName}\", skipping Init"); + return; + } + + Station = netPitStop; + plugToStation.Add(this, netPitStop); + + if (PluggableObject == null) + PluggableObject = GetComponent(); + + if (PluggableObject != null) + plugToNetworkedPluggable.Add(PluggableObject, this); + } + + public void DropPlug() + { + if (playerHolding != 0) + { + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out var player)) + { + player.DropItem(); + } + } + + playerHolding = 0; + PluggableObject.controlGrabbed = false; + BlockInteraction(false); + + PluggableObject.Unplug(); + PluggableObject.controlBase?.ForceEndInteraction(); + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + } + + public void GrabPlug(byte playerId) + { + playerHolding = playerId; + PluggableObject.controlGrabbed = true; + BlockInteraction(true); + + PluggableObject.Unplug(); + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + //attach to a player + if (NetworkLifecycle.Instance.IsClientRunning && + NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(playerHolding, out var player)) + { + var target = nonVrGrabAnchor?.GetGrabAnchor(); + Multiplayer.LogDebug(() => $"GrabPlug() NetId: {NetId}, player: {player.Username}, targetPos: {target?.localPos}, targetRot: {target?.localRot}"); + player.HoldItem(gameObject, target?.localPos, target?.localRot); + } + } + + private void DockTrainCar(ushort trainNetId, sbyte socketIndex) + { + DropPlug(); + + if (NetworkedTrainCar.TryGet(trainNetId, out NetworkedTrainCar netTrainCar)) + { + var socket = GetTrainCarSocket(netTrainCar, socketIndex); + if (socket == null) + { + Multiplayer.LogWarning($"Failed to dock plug in loco socket, socket not found! Plug NetId: {NetId}, TrainCar: [{netTrainCar.CurrentID}, {trainNetId}]"); + return; + } + + //bool result = PluggableObject.InstantSnapTo(socket); + bool result = PluggableObject.StartSnappingTo(socket, true); + + if (result) + { + TrainCarNetId = trainNetId; + SocketIndex = socketIndex; + } + + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}, isLeft: {socketIndex}, result: {result}"); + } + else + { + Multiplayer.LogDebug(() => $"ProcessPacket() NetId: {NetId}, DockSocket, trainCar: {trainNetId}. TrainCar not found!"); + } + } + + public PlugSocket GetTrainCarSocket(NetworkedTrainCar netTrainCar, sbyte socketIndex) + { + if (netTrainCar == null || netTrainCar.TrainCar == null) + return null; + + if (socketIndex < 0 || socketIndex >= netTrainCar.TrainCar.FuelSockets.Length) + { + Multiplayer.LogWarning($"Failed to find socket {socketIndex} in TrainCar: [{netTrainCar.CurrentID}, {netTrainCar.NetId}], index is out of bounds!"); + return null; + } + + return netTrainCar.TrainCar.FuelSockets[socketIndex]; + } + + private IEnumerator WaitForYankSettle() + { + Multiplayer.LogDebug(() => $"WaitForYankSettle() PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); + PluggableObject.yankOutOfHand = false; //block docking + + //allow force to be applied + yield return new WaitForFixedUpdate(); + Multiplayer.LogDebug(() => $"WaitForYankSettle() post-WaitForFixed, PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}"); + + float time = Time.time; + + //wait for rigid body to come to rest + yield return new WaitUntil(() => Mathf.Approximately(PlugRB.velocity.sqrMagnitude, 0.0f) || (Time.time - time > 2.0f)); + + Multiplayer.LogDebug(() => $"WaitForYankSettle() PluggableObject.yankOutOfHand: {PluggableObject.yankOutOfHand}, velocity: {PlugRB.velocity.sqrMagnitude}, delta Time: {Time.time - time}"); + + //wait for plug to come to rest (prevent docking home) + yield return new WaitForFixedUpdate(); + + PluggableObject.yankOutOfHand = true; //unblock docking + } + #endregion + + #region Client + private void OnGrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnGrabbed() post [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + currentInteraction = PlugInteractionType.PickedUp; + } + + private void OnUngrabbed(ControlImplBase control) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + //Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnUngrabbed() station: {Station?.StationName}, plugging state: {PluggableObject.State}"); + + // If we're snapping to a socket, don't send the Dropped packet + if (IsConnecting) + return; + + TrainCarNetId = INVALID_NETID; + SocketIndex = INVALID_SOCKET; + + currentInteraction = PlugInteractionType.Dropped; + } + + private void OnPluggedIn(PluggableObject plug, PlugSocket socket) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() station: {Station?.StationName}, processing: {NetworkLifecycle.Instance.IsProcessingPacket}, processing as Host: {processingAsHost}, refreshed: {Refreshed}, isConnecting: {IsConnecting}"); + + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + //Prevent new players/players entering the area from sending packets until initalised + if (!Refreshed) + return; + + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() [{transform.parent.name}, {NetId}] station: {Station?.StationName}"); + + PlugInteractionType interaction; + ushort carNetId = 0; + + if (socket == plug.startAttachedTo) + { + interaction = PlugInteractionType.DockHome; + SocketIndex = INVALID_SOCKET; + } + else + { + var trainCar = TrainCar.Resolve(socket.gameObject); + if (trainCar != null && trainCar.FuelSockets != null) + { + if (!NetworkedTrainCar.TryGetFromTrainCar(trainCar, out var netTrainCar)) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() NetworkedTrainCar: {trainCar?.ID} Not Found! Socket: {socket.GetObjectPath()}"); + return; + } + + carNetId = netTrainCar.NetId; + + interaction = PlugInteractionType.DockSocket; + SocketIndex = (sbyte)Array.IndexOf(trainCar.FuelSockets, socket); + + if (SocketIndex < 0) + Multiplayer.LogWarning(() => $"Socket not recognised for TrainCar [{trainCar.ID}, {netTrainCar.NetId}], socket: {socket.GetObjectPath()}"); + } + else + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.OnPlugged() Socket not recognised: {socket.GetObjectPath()}"); + return; + } + } + + currentInteraction = interaction; + TrainCarNetId = carNetId; + IsConnecting = false; + } + #endregion +} diff --git a/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs b/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs index 9d4f8c77..45f35ce3 100644 --- a/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs +++ b/Multiplayer/Components/Networking/World/NetworkedRailTrack.cs @@ -4,7 +4,46 @@ namespace Multiplayer.Components.Networking.World; public class NetworkedRailTrack : IdMonoBehaviour { - private static readonly Dictionary railTracksToNetworkedRailTracks = new(); + #region Lookup Cache + private static readonly Dictionary railTracksToNetworkedRailTracks = []; + + public static bool TryGet(ushort netId, out NetworkedRailTrack networkedRailTrack) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + networkedRailTrack = (NetworkedRailTrack)rawObj; + return b; + } + + public static bool TryGet(ushort netId, out RailTrack railTrack) + { + if (TryGet(netId, out NetworkedRailTrack networkedRailTrack)) + { + railTrack = networkedRailTrack.RailTrack; + return true; + } + + railTrack = null; + return false; + } + + public static bool TryGetNetId(RailTrack track, out ushort netId) + { + if (railTracksToNetworkedRailTracks.TryGetValue(track, out var networkedRailTrack)) + { + netId = networkedRailTrack.NetId; + return true; + } + + netId = 0; + return false; + } + + public static NetworkedRailTrack GetFromRailTrack(RailTrack railTrack) + { + return railTracksToNetworkedRailTracks[railTrack]; + } + + #endregion protected override bool IsIdServerAuthoritative => false; @@ -22,16 +61,4 @@ protected override void OnDestroy() base.OnDestroy(); railTracksToNetworkedRailTracks.Remove(RailTrack); } - - public static bool Get(ushort netId, out NetworkedRailTrack obj) - { - bool b = Get(netId, out IdMonoBehaviour rawObj); - obj = (NetworkedRailTrack)rawObj; - return b; - } - - public static NetworkedRailTrack GetFromRailTrack(RailTrack railTrack) - { - return railTracksToNetworkedRailTracks[railTrack]; - } } diff --git a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs deleted file mode 100644 index 8e1c4992..00000000 --- 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 00000000..208d6d1b --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -0,0 +1,628 @@ +using DV.Booklets; +using DV.Logic.Job; +using DV.ServicePenalty; +using DV.ThingTypes; +using DV.Utils; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Collections; +using System; +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 bool TryGet(ushort netId, out StationController stationController) + { + if (Get(netId, out var networkedStationController)) + { + stationController = networkedStationController.StationController; + return true; + } + + stationController = null; + return false; + } + + public static bool TryGet(ushort netId, out Station station) + { + if (Get(netId, out var networkedStationController)) + { + station = networkedStationController.StationController.logicStation; + return true; + } + + station = null; + return false; + } + + public static bool TryGet(ushort netId, out JobValidator jobValidator) + { + if (Get(netId, out var networkedStationController)) + { + jobValidator = networkedStationController.JobValidator; + return true; + } + + jobValidator = null; + return false; + } + + public static bool TryGetNetId(StationController stationController, out ushort netId) + { + if (GetFromStationController(stationController, out var networkedStationController)) + { + netId = networkedStationController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGetNetId(Station station, out ushort netId) + { + if (GetFromStation(station, out var networkedStationController)) + { + netId = networkedStationController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static bool TryGetNetId(JobValidator jobValidator, out ushort netId) + { + if (GetFromJobValidator(jobValidator, out var networkedStationController)) + { + netId = networkedStationController.NetId; + return true; + } + + netId = 0; + return false; + } + + public static Dictionary GetAll() + { + 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) + { + var newJobData = JobData.ToJob(jobData); + + Job newJob = newJobData.newJob; + var netIdToTask = newJobData.netIdToTask; + + var carNetIds = jobData.GetCars(); + + NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds, netIdToTask); + 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) + { + StationController.logicStation.AddJobToStation(newJob); + StationController.processedNewJobs.Add(newJob); + + takenJobs.Add(newJob); + newJob.TakeJob(true); //take job as if loaded from save to prevent debt controller kicking in + } + 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 IEnumerator DelayCreateJob(JobData jobData) + { + int frameCounter = 0; + + Multiplayer.LogDebug(() => $"DelayCreateJob([{jobData.NetID}, {jobData.ID}]) job type: {jobData.JobType}"); + + yield return new WaitForEndOfFrame(); + + while (frameCounter < MAX_FRAMES) + { + if (CheckCarsLoaded(jobData)) + { + Multiplayer.LogDebug(() => $"DelayCreateJob([{jobData.NetID}, {jobData.ID}]) 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}, {jobData.ID}]"); + } + + 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.TryGet(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, Dictionary netIdToTask) + { + NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); + networkedJob.NetId = netId; + networkedJob.Initialize(job, this); + networkedJob.SetTasksFromServer(netIdToTask); + 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; + + netJob.Job.startTime = job.StartTime; + netJob.Job.finishTime = job.FinishTime; + + UpdateJobState(netJob, job); + UpdateJobOverview(netJob, job); + + } + } + + 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); + + netJob.Job.TakeJob(true); //take job as if loaded from save to prevent debt controller kicking in + + 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); + netJob.Job.CompleteJob(); + + 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); + netJob.Job.AbandonJob(); + 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.TryGet(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},{networkedJob.Job.jobType}], {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 ad2b0dd8..d1655d93 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -1,24 +1,59 @@ -using System.Linq; using DV; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace Multiplayer.Components.Networking.World; public class NetworkedTurntable : IdMonoBehaviour { + #region Lookup Cache private static NetworkedTurntable[] _indexedTurntables; - public static NetworkedTurntable[] IndexedTurntables => _indexedTurntables ??= WorldData.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + private static readonly Dictionary turntableToNetworkedTurntable = []; + public static NetworkedTurntable[] IndexedTurntables => _indexedTurntables ??= RailTrackRegistry.Instance.TrackRootParent.GetComponentsInChildren().OrderBy(nj => nj.NetId).ToArray(); + + + public static bool TryGet(ushort netId, out TurntableRailTrack turntable) + { + if (Get((byte)netId, out var networkedTurntable)) + { + turntable = networkedTurntable.TurntableRailTrack; + return true; + } + + turntable = null; + return false; + } + + public static bool TryGetNetId(TurntableRailTrack turntable, out ushort netId) + { + if (turntableToNetworkedTurntable.TryGetValue(turntable, out var networkedTurntable)) + { + netId = networkedTurntable.NetId; + return true; + } + + netId = 0; + return false; + } + + #endregion protected override bool IsIdServerAuthoritative => false; public TurntableRailTrack TurntableRailTrack; private float lastYRotation; + private bool initialised = false; protected override void Awake() { base.Awake(); TurntableRailTrack = GetComponent(); + turntableToNetworkedTurntable[TurntableRailTrack] = this; + NetworkLifecycle.Instance.OnTick += OnTick; + + initialised = NetworkLifecycle.Instance.IsHost(); } protected override void OnDestroy() @@ -26,23 +61,29 @@ protected override void OnDestroy() base.OnDestroy(); if (UnloadWatcher.isQuitting) return; + + turntableToNetworkedTurntable.Remove(TurntableRailTrack); + NetworkLifecycle.Instance.OnTick -= OnTick; } private void OnTick(uint tick) { - if (Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation)) + if (UnloadWatcher.isUnloading || !initialised || Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation)) return; lastYRotation = TurntableRailTrack.targetYRotation; NetworkLifecycle.Instance.Client.SendTurntableRotation(NetId, lastYRotation); } - public void SetRotation(float rotation, bool forceConnectionRefresh = false) + public void SetRotation(float rotation, bool forceConnectionRefresh = false, bool initialising = false) { lastYRotation = rotation; TurntableRailTrack.targetYRotation = rotation; TurntableRailTrack.RotateToTargetRotation(forceConnectionRefresh); + + if (!initialised && initialising) + initialised = true; } public static bool Get(byte netId, out NetworkedTurntable obj) diff --git a/Multiplayer/Components/SaveGame/Client_GameSession.cs b/Multiplayer/Components/SaveGame/Client_GameSession.cs new file mode 100644 index 00000000..4c0fe2b5 --- /dev/null +++ b/Multiplayer/Components/SaveGame/Client_GameSession.cs @@ -0,0 +1,99 @@ +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.Reflection; +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; + } + + bool IGameSession.CanCreateNewSaves(SaveType saveType) + { + return false; + } +} diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index af0b5df4..f93873fe 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -1,4 +1,3 @@ -using System; using DV.InventorySystem; using DV.JObjectExtstensions; using DV.ThingTypes; @@ -6,8 +5,8 @@ using JetBrains.Annotations; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Listeners; using Newtonsoft.Json.Linq; +using System; namespace Multiplayer.Components.SaveGame; @@ -24,6 +23,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 +63,18 @@ 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) + if (player.Peer == NetworkLifecycle.Instance.Server.SelfPeer || !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 c764c937..8430ecf8 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -1,6 +1,15 @@ using System; using System.Collections; +using System.ComponentModel; +using System.Linq; +using DV; +using DV.CabControls; +using DV.Common; +using DV.Logic.Job; 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 +23,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 +46,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,16 +80,23 @@ 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); LicenseManager.Instance.LoadData(saveGameData); + Multiplayer.Log("Waiting for Logic Controller..."); + yield return new WaitUntil(() => LogicController.Instance.initialized); + Multiplayer.Log("Logic Controller initialised."); + + JobsManager.Instance.LoadTime(packet.JobManagerTime); + 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 +129,4 @@ public override bool ShouldCreateSaveGameAfterLoad() public override void MakeCurrent(){} } + diff --git a/Multiplayer/Components/UI/Controls/MPGridView.cs b/Multiplayer/Components/UI/Controls/MPGridView.cs new file mode 100644 index 00000000..5c8b72c4 --- /dev/null +++ b/Multiplayer/Components/UI/Controls/MPGridView.cs @@ -0,0 +1,447 @@ +using DV.UIFramework; +using Multiplayer.Components.MainMenu; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.UI.Controls; + +public class MPGridView : AUIView +{ + public delegate void IndexChangeDelegate(MPGridView sender); + public event IndexChangeDelegate SelectedIndexChanged; + public event IndexChangeDelegate HoveredIndexChanged; + + // Core properties + public GameObject viewElementPrefab; + public GameObject placeholderElementPrefab; + public bool showPlaceholderWhenEmpty = true; + public bool allowHoveringAndSelecting = true; + + // Internal state + private readonly List _items = []; + private MPViewElement _selectedItem; + private MPViewElement _hoveredItem; + private bool _placeholderVisible = false; + private bool previousInteractability; + + // Components + private ScrollRect _scrollBar; + + + // Gridview properties + public IReadOnlyList Items => _items.AsReadOnly(); + public int SelectedIndex { get; private set; } + public int HoveredIndex { get; private set; } + + public T SelectedItem + { + get + { + return (SelectedIndex >= 0 && SelectedIndex < _items.Count) ? + _items[SelectedIndex] + : default; + } + } + + // Item access methods + public T this[int index] + { + get + { + if (index < 0 || index >= _items.Count) + return default; + return _items[index]; + } + } + + public bool Contains(T item) => _items.Contains(item); + public int IndexOf(T item) => _items.IndexOf(item); + public int FindIndex(Predicate match) => _items.FindIndex(match); + public T Find(Predicate match) => _items.Find(match); + + public void Clear() + { + // Clear selection + _selectedItem = null; + _hoveredItem = null; + SelectedIndex = -1; + HoveredIndex = -1; + + // Remove all child elements + for (int i = transform.childCount - 1; i >= 0; i--) + { + Destroy(transform.GetChild(i).gameObject); + } + + // Clear items list + _items.Clear(); + _placeholderVisible = false; + + // Show placeholder if needed + UpdatePlaceholder(); + + // Notify selection changed + SelectedIndexChanged?.Invoke(this); + } + + // Add a single item + public void AddItem(T item) + { + _items.Add(item); + CreateViewElement(item); + UpdatePlaceholder(); + } + + // Add multiple items + public void AddItems(IEnumerable items) + { + if (items == null) + return; + + foreach (var item in items) + { + _items.Add(item); + CreateViewElement(item); + } + + UpdatePlaceholder(); + } + + // Remove an item + public void RemoveItem(T item) + { + int index = _items.IndexOf(item); + if (index >= 0) + { + RemoveItemAt(index); + } + } + + // Remove an item at a specific index + public void RemoveItemAt(int index) + { + if (index < 0 || index >= _items.Count || _placeholderVisible) + return; + + // Check if we're removing the selected item + if (_selectedItem != null && _selectedItem.transform.GetSiblingIndex() == index) + { + _selectedItem = null; + SelectedIndexChanged?.Invoke(this); + } + + // Remove the view element + if (index < transform.childCount) + { + + Destroy(transform.GetChild(index).gameObject); + } + + // Remove from items list + _items.RemoveAt(index); + + // Update placeholder + UpdatePlaceholder(); + } + + // Sort items using a comparison function + public void SortItems(Comparison comparison) + { + if (_items.Count <= 1) + return; + + // Remember selected item + T selectedItem = SelectedItem; + + // Sort the items list + _items.Sort(comparison); + + // Rebuild view elements + for (int i = transform.childCount - 1; i >= 0; i--) + { + // Skip placeholder if it exists + if (_placeholderVisible && i == 0) + continue; + + Destroy(transform.GetChild(i).gameObject); + } + + // Recreate view elements in sorted order + foreach (var item in _items) + { + CreateViewElement(item); + } + + // Restore selection if possible + if (selectedItem != null) + { + int newIndex = _items.IndexOf(selectedItem); + if (newIndex >= 0) + { + SetSelected(newIndex, true); + } + } + } + + // Set selected item by index + public void SetSelected(int index, bool scrollToItem = true) + { + Multiplayer.LogDebug(() => $"MPGridView.SetSelected({index}, {scrollToItem}) child count: {transform.childCount}"); + + if (index < 0 || index >= _items.Count || _placeholderVisible) + return; + + Multiplayer.LogDebug(() => $"MPGridView.SetSelected({index}, {scrollToItem}) items count: {index}"); + + if (index >= transform.childCount) + return; + + Transform child = transform.GetChild(index); + MPViewElement element = child.GetComponent>(); + + Multiplayer.LogDebug(() => + { + var el = element as IServerBrowserGameDetails; + return $"MPGridView.SetSelected({index}, {scrollToItem}) {element?.name}"; + + }); + + if (element != null) + { + UpdateSelectedItem(element); + } + + if (scrollToItem) + ScrollToItem(); + } + + private void ScrollToItem() + { + if (_scrollBar != null && _items.Count > 0 && SelectedIndex >= 0) + { + if (_scrollBar.content != null && _scrollBar.viewport != null && + _selectedItem && _selectedItem.TryGetComponent(out var itemRect) + ) + { + // Get the content RectTransform + RectTransform contentRect = _scrollBar.content; + + // Calculate the normalized position based on the item's position within the content + float itemPosition = itemRect.anchoredPosition.y; + float contentHeight = contentRect.rect.height; + + if (contentHeight == 0) + return; + + // Adjust for the viewport height to center the item + float viewportHeight = _scrollBar.viewport.rect.height; + float adjustment = viewportHeight * 0.5f / contentHeight; + + // Set the normalized position (clamped between 0 and 1) + float normalizedPos = Mathf.Clamp01(itemPosition / contentHeight + adjustment); + _scrollBar.verticalNormalizedPosition = 1f - normalizedPos; + } + else if(_items.Count != 0) + { + _scrollBar.verticalNormalizedPosition = 1f - (float)SelectedIndex / (float)_items.Count; + } + } + } + + // Get view element at index + public MPViewElement GetElementAt(int index) + { + if (index < 0 || index >= _items.Count || _placeholderVisible) + return null; + + if (index < transform.childCount) + { + return transform.GetChild(index).GetComponent>(); + } + + return null; + } + + // Create a view element for an item + private GameObject CreateViewElement(T item) + { + if (viewElementPrefab == null) + return null; + + GameObject element = Instantiate(viewElementPrefab, transform); + MPViewElement viewElement = element.GetComponent>(); + + viewElement.SetData(item); + viewElement.SetInteractable(allowHoveringAndSelecting); + viewElement.SelectionRequested += UpdateSelectedItem; + viewElement.HoverChanged += UpdateHoverState; + + return element; + } + + // Create placeholder element + private GameObject CreatePlaceholderElement() + { + if (placeholderElementPrefab == null) + return null; + + GameObject element = Instantiate(placeholderElementPrefab, transform); + element.transform.SetAsFirstSibling(); + + MPViewElement viewElement = element.GetComponent>(); + viewElement.SetInteractable(false); + + return element; + } + + // Update placeholder visibility + private void UpdatePlaceholder() + { + bool shouldShowPlaceholder = _items.Count == 0 && showPlaceholderWhenEmpty; + + // If placeholder state hasn't changed, do nothing + if (_placeholderVisible == shouldShowPlaceholder) + return; + + _placeholderVisible = shouldShowPlaceholder; + + // Remove existing placeholder if it exists + if (!shouldShowPlaceholder && transform.childCount > 0) + { + // Check for any placeholder + MPViewElement[] placeholders = transform.GetComponentsInChildren>().Where(e => e.IsPlaceholder).ToArray(); + + for (int i = 0; i < placeholders.Length; i++) + Destroy(placeholders[i].gameObject); + } + + // Add placeholder if needed + if (shouldShowPlaceholder) + { + CreatePlaceholderElement(); + } + } + + // Handle selection changes + private void UpdateSelectedItem(MPViewElement element) + { + _selectedItem?.SetSelected(false); + + if (_placeholderVisible) + { + _selectedItem = null; + SelectedIndex = -1; + HoveredIndex = -1; + SelectedIndexChanged?.Invoke(this); + return; + } + + _selectedItem = element; + _selectedItem.SetSelected(true); + + SelectedIndex = element.transform.GetSiblingIndex(); + + SelectedIndexChanged?.Invoke(this); + } + + // Handle hover state changes + private void UpdateHoverState(MPViewElement element, bool hovered) + { + _hoveredItem = hovered ? element : null; + + if (_hoveredItem != null) + HoveredIndex = element.transform.GetSiblingIndex(); + else + HoveredIndex = -1; + + HoveredIndexChanged?.Invoke(this); + } + + // Update interactability of all elements + private void UpdateInteractability() + { + foreach (Transform child in transform) + { + MPViewElement element = child.GetComponent>(); + if (element != null) + { + element.SetInteractable(allowHoveringAndSelecting); + + if (!allowHoveringAndSelecting) + { + element.SetSelected(false); + } + } + } + } + + protected virtual void Awake() + { + ValidatePrefabs(); + + if (_scrollBar == null) + _scrollBar = GetComponentInParent(); + } + + protected virtual void OnValidate() + { + ValidatePrefabs(); + } + + private void ValidatePrefabs() + { + if (viewElementPrefab != null) + { + var viewElement = viewElementPrefab.GetComponent>(); + if (viewElement == null) + { + Multiplayer.LogError($"View element prefab must have an MPViewElement<{typeof(T).Name}> component"); + viewElementPrefab = null; + } + else if (viewElement.IsPlaceholder) + { + Multiplayer.LogError($"View element prefab must not be a placeholder"); + viewElementPrefab = null; + } + + if (viewElementPrefab.GetComponent() == null) + { + Multiplayer.LogError($"View element prefab must have an IMarkable component"); + viewElementPrefab = null; + } + } + + if (placeholderElementPrefab != null) + { + var placeholderElement = placeholderElementPrefab.GetComponent>(); + if (placeholderElement == null) + { + Multiplayer.LogError($"Placeholder element prefab must have an MPViewElement<{typeof(T).Name}> component"); + placeholderElementPrefab = null; + } + else if (placeholderElement.IsPlaceholder == false) + { + Multiplayer.LogError($"Placeholder element prefab must be a placeholder"); + placeholderElementPrefab = null; + } + + if (placeholderElementPrefab.GetComponent() == null) + { + Multiplayer.LogError($"Placeholder element prefab must have an IMarkable component"); + placeholderElementPrefab = null; + } + } + } + + protected void Update() + { + if (previousInteractability != allowHoveringAndSelecting) + { + previousInteractability = allowHoveringAndSelecting; + UpdateInteractability(); + } + } +} diff --git a/Multiplayer/Components/UI/Controls/MPViewElement.cs b/Multiplayer/Components/UI/Controls/MPViewElement.cs new file mode 100644 index 00000000..aba43458 --- /dev/null +++ b/Multiplayer/Components/UI/Controls/MPViewElement.cs @@ -0,0 +1,61 @@ +using DV.UIFramework; +using System; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace Multiplayer.Components.UI.Controls; + +[RequireComponent(typeof(IMarkable))] +public abstract class MPViewElement : NullCheckingMonoBehaviour, ISelectHandler, IEventSystemHandler, IPointerEnterHandler, IPointerExitHandler +{ + public event Action> SelectionRequested; + public event Action, bool> HoverChanged; + + public abstract bool IsPlaceholder { get; } + + public virtual void SetSelected(bool selected) + { + //Multiplayer.LogDebug(() => + //{ + // var data = GetComponent(); + // return $"MPViewElement.SetSelected() {data?.name}"; + //}); + + if (TryGetComponent(out var component)) + { + component.ToggleMarked(selected); + } + } + + public virtual void SetInteractable(bool interactable) + { + if (TryGetComponent(out var component)) + { + component.ToggleInteractable(interactable); + } + } + + public virtual void OnSelect(BaseEventData eventData) + { + + //Multiplayer.LogDebug(()=> + //{ + // var data = GetComponent(); + // return $"MPViewElement.OnSelect() {data?.name}"; + //}); + + SelectionRequested?.Invoke(this); + } + + public virtual void OnPointerEnter(PointerEventData eventData) + { + HoverChanged?.Invoke(this, arg2: true); + } + + public virtual void OnPointerExit(PointerEventData eventData) + { + HoverChanged?.Invoke(this, arg2: false); + } + + public abstract void SetData(T data); +} diff --git a/Multiplayer/Components/Util/HyperlinkHandler.cs b/Multiplayer/Components/Util/HyperlinkHandler.cs new file mode 100644 index 00000000..e8d8a149 --- /dev/null +++ b/Multiplayer/Components/Util/HyperlinkHandler.cs @@ -0,0 +1,155 @@ +using System.Text.RegularExpressions; +using TMPro; +using UnityEngine; +using UnityEngine.EventSystems; + +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; + + protected void Awake() + { + InitializeComponents(); + } + + protected void Start() + { + InitializeComponents(); + ApplyLinkStyling(); + } + + private void InitializeComponents() + { + if (textComponent == null) + { + textComponent = GetComponent(); + + textComponent.raycastTarget = true; + } + + if (canvas == null) + { + canvas = GetComponentInParent(); + if (canvas != null) + { + canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; + } + } + } + + protected void Update() + { + if (textComponent == null || canvas == null) + return; + + 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) + { + if (textComponent == null) + return; + + Multiplayer.LogDebug(() => $"HyperlinkHandler.OnPointerClick() mouse pos: {Input.mousePosition}"); + int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); + + if (linkIndex != -1) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + string url = linkInfo.GetLinkID(); + Multiplayer.LogDebug(() => $"HyperlinkHandler: Opening URL: {url}"); + Application.OpenURL(url); + } + } + + public void ApplyLinkStyling() + { + if (textComponent == null) + return; + + 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 e6d15447..d8cb51e8 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -5,149 +5,302 @@ using I2.Loc; using Multiplayer.Utils; -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"; + public static class Locale + { + 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"; + + public static string MAIN_MENU__INCOMPATIBLE_MODS => Get(MAIN_MENU__INCOMPATIBLE_MODS_KEY); + public const string MAIN_MENU__INCOMPATIBLE_MODS_KEY = $"{PREFIX_MAIN_MENU}/incompatible_mods"; + + public static string MAIN_MENU__UPDATE_TITLE => Get(MAIN_MENU__MAIN_MENU__UPDATE_TITLE_KEY); + public const string MAIN_MENU__MAIN_MENU__UPDATE_TITLE_KEY = $"{PREFIX_MAIN_MENU}/update_title"; + + public static string MAIN_MENU__UPDATE_LATEST => Get(MAIN_MENU__UPDATE_LATEST_KEY); + public const string MAIN_MENU__UPDATE_LATEST_KEY = $"{PREFIX_MAIN_MENU}/update_latest"; + + public static string MAIN_MENU__UPDATE_INSTALLED => Get(MAIN_MENU__UPDATE_INSTALLED_KEY); + public const string MAIN_MENU__UPDATE_INSTALLED_KEY = $"{PREFIX_MAIN_MENU}/update_installed"; + + public static string MAIN_MENU__UPDATE_ACTION => Get(MAIN_MENU__UPDATE_ACTION_KEY); + public const string MAIN_MENU__UPDATE_ACTION_KEY = $"{PREFIX_MAIN_MENU}/update_action"; + #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__PASSWORD_REQUIRED_YES => Get(SERVER_BROWSER__PASSWORD_REQUIRED_YES_KEY); + private const string SERVER_BROWSER__PASSWORD_REQUIRED_YES_KEY = $"{PREFIX_SERVER_BROWSER}/password_required_yes"; + public static string SERVER_BROWSER__PASSWORD_REQUIRED_NO => Get(SERVER_BROWSER__PASSWORD_REQUIRED_NO_KEY); + private const string SERVER_BROWSER__PASSWORD_REQUIRED_NO_KEY = $"{PREFIX_SERVER_BROWSER}/password_required_no"; + + 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__OK => Get(SERVER_BROWSER__OK_KEY); + private const string SERVER_BROWSER__OK_KEY = $"{PREFIX_SERVER_BROWSER}/ok"; + public static string SERVER_BROWSER__MISMATCH => Get(SERVER_BROWSER__MISMATCH_KEY); + private const string SERVER_BROWSER__MISMATCH_KEY = $"{PREFIX_SERVER_BROWSER}/mismatch"; + public static string SERVER_BROWSER__MISSING => Get(SERVER_BROWSER__MISSING_KEY); + private const string SERVER_BROWSER__MISSING_KEY = $"{PREFIX_SERVER_BROWSER}/missing"; + + public static string SERVER_BROWSER__REQUIRED_MODS => Get(SERVER_BROWSER__REQUIRED_MODS_KEY); + private const string SERVER_BROWSER__REQUIRED_MODS_KEY = $"{PREFIX_SERVER_BROWSER}/required_mods"; + + public static string SERVER_BROWSER__EXTRA_MODS => Get(SERVER_BROWSER__EXTRA_MODS_KEY); + private const string SERVER_BROWSER__EXTRA_MODS_KEY = $"{PREFIX_SERVER_BROWSER}/extra_mods"; + public static string SERVER_BROWSER__INCOMPATIBLE => Get(SERVER_BROWSER__INCOMPATIBLE_KEY); + private const string SERVER_BROWSER__INCOMPATIBLE_KEY = $"{PREFIX_SERVER_BROWSER}/incompatible"; + public static string SERVER_BROWSER__EXTRA_MOD => Get(SERVER_BROWSER__EXTRA_MOD_KEY); + private const string SERVER_BROWSER__EXTRA_MOD_KEY = $"{PREFIX_SERVER_BROWSER}/extra_mod"; + 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_VISIBILITY => Get(SERVER_HOST_PUBLIC_KEY); + public const string SERVER_HOST_VISIBILITY_KEY = $"{PREFIX_SERVER_HOST}/visibility"; + + public static string[] SERVER_HOST_VISIBILITY_MODES = [$"{SERVER_HOST_VISIBILITY_MODES_KEY}/private", $"{SERVER_HOST_VISIBILITY_MODES_KEY}/friends", $"{SERVER_HOST_VISIBILITY_MODES_KEY}/public"]; + public const string SERVER_HOST_VISIBILITY_MODES_KEY = $"{PREFIX_SERVER_HOST}/visibility/modes"; + 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__UNKNOWN_HOST => Get(DISCONN_REASON__UNKNOWN_HOST_KEY); +public const string DISCONN_REASON__UNKNOWN_HOST_KEY = $"{PREFIX_DISCONN_REASON}/unknown"; + +public static string DISCONN_REASON__HOST_UNREACHABLE => Get(DISCONN_REASON__HOST_UNREACHABLE_KEY); +public const string DISCONN_REASON__HOST_UNREACHABLE_KEY = $"{PREFIX_DISCONN_REASON}/unreachable"; + +public static string DISCONN_REASON__HOST_TIMED_OUT => Get(DISCONN_REASON__HOST_TIMED_OUT_KEY); +public const string DISCONN_REASON__HOST_TIMED_OUT_KEY = $"{PREFIX_DISCONN_REASON}/timeout"; + +public static string DISCONN_REASON__REJECTED => Get(DISCONN_REASON__REJECTED_KEY); +public const string DISCONN_REASON__REJECTED_KEY = $"{PREFIX_DISCONN_REASON}/rejected"; + +public static string DISCONN_REASON__SHUTTING_DOWN => Get(DISCONN_REASON__SHUTTING_DOWN_KEY); +public const string DISCONN_REASON__SHUTTING_DOWN_KEY = $"{PREFIX_DISCONN_REASON}/shutdown"; + +public static string DISCONN_REASON__PLAYER_KICKED => Get(DISCONN_REASON__PLAYER_KICKED_KEY); +public const string DISCONN_REASON__PLAYER_KICKED_KEY = $"{PREFIX_DISCONN_REASON}/kicked"; + +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"; + +public static string DISCONN_REASON__MODS_INCOMPATIBLE => Get(DISCONN_REASON__MODS_INCOMPATIBLE_KEY); +public const string DISCONN_REASON__MODS_INCOMPATIBLE_KEY = $"{PREFIX_DISCONN_REASON}/mods_incompatible"; +#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 - #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"; - #region Player List +public static string CHAT_WHISPER_NOT_FOUND => Get(CHAT_WHISPER_NOT_FOUND_KEY); +public const string CHAT_WHISPER_NOT_FOUND_KEY = $"{PREFIX_CHAT_INFO}/whisper/not_found"; - public static string PLAYER_LIST__TITLE => Get(PLAYER_LIST__TITLE_KEY); - private const string PLAYER_LIST__TITLE_KEY = $"{PREFIX_PLAYER_LIST}/title"; +public static string CHAT_KICK_UNABLE => Get(CHAT_KICK_UNABLE_KEY); +public const string CHAT_KICK_UNABLE_KEY = $"{PREFIX_CHAT_INFO}/kick/unable"; +public static string CHAT_KICK_KICKED => Get(CHAT_KICK_KICKED_KEY); +public const string CHAT_KICK_KICKED_KEY = $"{PREFIX_CHAT_INFO}/kick/kicked"; - #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"; +#endregion - 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"; +#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"; - #endregion +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; +private static bool initializeAttempted; +private static ReadOnlyDictionary> csv; - public static void Load(string localeDir) +public static void Load(string localeDir) +{ + initializeAttempted = true; + string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); + if (!File.Exists(path)) { - 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)}"); + Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); + return; } - public static string Get(string key, string overrideLanguage = null) - { - if (!initializeAttempted) - throw new InvalidOperationException("Not initialized"); + csv = Csv.Parse(File.ReadAllText(path)); + //Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); +} - if (csv == null) - return MISSING_TRANSLATION; +public static string Get(string key, string overrideLanguage = null) +{ + if (!initializeAttempted) + throw new InvalidOperationException("Not initialized"); - 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}"); - } + if (csv == null) + return MISSING_TRANSLATION; - Dictionary localeDict = csv[locale]; - string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; - if (localeDict.TryGetValue(actualKey, out string value)) + string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; + if (!csv.ContainsKey(locale)) + { + if (locale == DEFAULT_LANGUAGE) { - if (value == string.Empty) - return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; - return 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; } - Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); - return MISSING_TRANSLATION; + locale = DEFAULT_LANGUAGE; + Multiplayer.LogWarning($"Failed to find locale language {locale}"); } - public static string Get(string key, params object[] placeholders) + Dictionary localeDict = csv[locale]; + string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; + if (localeDict.TryGetValue(actualKey, out string value)) { - return string.Format(Get(key), placeholders); + if (string.IsNullOrEmpty(value)) + return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; + return value; } - public static string Get(string key, params string[] placeholders) - { - // ReSharper disable once CoVariantArrayConversion - return Get(key, (object[])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) +{ + return Get(key, (object[])placeholders); +} +} } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 87ca8b09..26773f76 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,11 +1,20 @@ -using System; -using System.IO; +using DV; +using DV.UIFramework; using HarmonyLib; using JetBrains.Annotations; +using LiteNetLib; +using MPAPI; +using Multiplayer.API; +using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Editor; using Multiplayer.Patches.Mods; using Multiplayer.Patches.World; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; using UnityChan; using UnityEngine; using UnityModManagerNet; @@ -16,19 +25,38 @@ 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 APIProvider _apiProvider; 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 string LocalBuildInfo => BuildInfo.BUILD_VERSION_MAJOR.ToString() + " - " + BuildInfo.BUILDBOT_INFO; + + + public static bool specLog = false; [UsedImplicitly] - private static bool Load(UnityModManager.ModEntry modEntry) + public 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,8 +66,33 @@ private static bool Load(UnityModManager.ModEntry modEntry) Locale.Load(ModEntry.Path); + var gameVer = BuildInfo.BUILD_VERSION_MAJOR.ToString() + + (string.IsNullOrEmpty(BuildInfo.BUILD_VERSION_SUFFIX) ? "" : "." + BuildInfo.BUILD_VERSION_SUFFIX); + + bool APIcompatible = false; + if (Version.TryParse(APIProvider.BUILT_AGAINST_API_VERSION, out var builtVerAPI) && Version.TryParse(MultiplayerAPI.LoadedApiVersion, out var loadedVerAPI)) + { + APIcompatible = loadedVerAPI >= builtVerAPI; + } + + Log($"\r\n\r\n" + + $"\tMultiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\n" + + $"\tGame Version: {gameVer}\r\n" + + $"\tBuildbot Version: {BuildInfo.BUILDBOT_INFO.ToString()}\r\n" + + $"\tLiteNetLib Version: {LiteNetLibVer()}\r\n" + + $"\tMultiplayer API Required Version: {APIProvider.BUILT_AGAINST_API_VERSION}, Loaded Version: {MultiplayerAPI.LoadedApiVersion}\r\n" + + $"\tMultiplayer API Compatible: {APIcompatible}\r\n"); + + if (!APIcompatible) + { + throw new Exception("Multiplayer API version mismatch! One or more mods are using a newer version of the Multiplayer API, please update Multiplayer Mod or disable these mods.\r\n"); + } + Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); +#if DEBUG + Harmony.DEBUG = true; +#endif harmony.PatchAll(); SimComponent_Tick_Patch.Patch(harmony); @@ -60,6 +113,18 @@ private static bool Load(UnityModManager.ModEntry modEntry) Log("Creating NetworkManager..."); NetworkLifecycle.CreateLifecycle(); + + Log("Loading Compatibility Manager..."); + ModCompatibilityManager.Instance.CheckInstance(); + + Log("Loading API Provider..."); + _apiProvider = new APIProvider(); + MultiplayerAPI.RegisterAPI(_apiProvider); + +#if DEBUG + CheckPatches(); +#endif + } catch (Exception ex) { @@ -99,6 +164,87 @@ public static bool LoadAssets() return true; } + private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTime) + { + if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") + { +#if DEBUG + CheckPatches(); +#endif + 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."; + */ + + var latestVer = Locale.Get(Locale.MAIN_MENU__UPDATE_LATEST_KEY, [$"\t\t{ModEntry.NewestVersion}"]); + var installedVer = Locale.Get(Locale.MAIN_MENU__UPDATE_INSTALLED_KEY, [$"\t\t{ModEntry.Version}"]); + + update.labelTMPro.text = Locale.MAIN_MENU__UPDATE_TITLE + + $"\r\n\r\n{latestVer}" + + $"\r\n{installedVer}\r\n\r\n" + + $"{Locale.MAIN_MENU__UPDATE_ACTION}"; + + 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; + + + } + } + } + } + + static string LiteNetLibVer() + { + Assembly liteNetLibAssembly = typeof(NetManager).Assembly; + AssemblyName assemblyName = liteNetLibAssembly.GetName(); + + return assemblyName.Version.ToString(); + } +#if DEBUG + public static void CheckPatches() + { + StringBuilder sb = new StringBuilder("Harmony patches:"); + foreach (var info in Harmony.GetAllPatchedMethods()) + { + var patches = Harmony.GetPatchInfo(info); + sb.Append($"\r\n- {info.DeclaringType.FullName}.{info.Name} patched by:"); + foreach (var p in patches.Prefixes) + sb.Append($"\r\n - Prefix: {p.PatchMethod.DeclaringType.FullName}.{p.PatchMethod.Name}"); + foreach (var p in patches.Postfixes) + sb.Append($"\r\n - Postfix: {p.PatchMethod.DeclaringType.FullName}.{p.PatchMethod.Name}"); + } + + LogDebug(()=>sb.ToString()); + } +#endif + + #region Logging public static void LogDebug(Func resolver) @@ -130,7 +276,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 70448c48..5fcb89f4 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,35 +1,57 @@ - + net48 latest Multiplayer + 0.1.13.5 - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + - - + + + $(NoWarn);CS0436 + + + + + + + + @@ -40,54 +62,64 @@ - - + - - + + + - - ../build/LiteNetLib.dll - ../build/MultiplayerEditor.dll + + ../build/UnityChan.dll + - - + + + + + + + - - - - - + + + + + + + + + + + - - - - - + + + + diff --git a/Multiplayer/Networking/Data/BogieData.cs b/Multiplayer/Networking/Data/BogieData.cs deleted file mode 100644 index 33f6851e..00000000 --- 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 00000000..e8d34e73 --- /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 00000000..3c8e8c4c --- /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 00000000..ddedfe06 --- /dev/null +++ b/Multiplayer/Networking/Data/JobData.cs @@ -0,0 +1,270 @@ +using DV.Logic.Job; +using DV.ThingTypes; +using LiteNetLib.Utils; +using MPAPI.Types; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; + +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.Value})"); + + 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 (Job newJob, Dictionary netIdToTask) ToJob(JobData jobData) + { + Dictionary netIdToTask = []; + + List tasks = jobData.Tasks.Select(taskData => taskData.ToTask(ref netIdToTask)).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 new(newJob, netIdToTask); + } + + 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) + { + bw.Write((byte)task.TaskType); + + using (MemoryStream taskMemStream = new()) + using (BinaryWriter taskSerialiser = new(taskMemStream)) + { + task.Serialize(taskSerialiser); + + if (taskMemStream.Length > int.MaxValue) + { + Multiplayer.LogError($"Task {task.TaskType} too large: {taskMemStream.Length}"); + throw new InvalidOperationException($"Task {task.TaskType} data is too large to serialize."); + } + + bw.Write((int)taskMemStream.Length); + bw.Write(taskMemStream.ToArray()); + } + } + + 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) + { + ushort netID = 0; + JobType jobType = JobType.Custom; + string id = string.Empty; + + try + { + netID = reader.GetUShort(); + jobType = (JobType)reader.GetByte(); + 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(); + + using (MemoryStream taskStream = new MemoryStream(br.ReadBytes(taskLength))) + using (BinaryReader taskReader = new BinaryReader(taskStream)) + { + 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! netId: {netID}, jobId: {id} {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 00000000..c71c56d2 --- /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 00000000..f2b59ec2 --- /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 00000000..e06707f8 --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -0,0 +1,174 @@ +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 ModInfo[] 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 ServerVisibility Visibility { get; set; } = ServerVisibility.Public; + [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) + { + // Data available flag + writer.Put(data != null); + + if (data != null) + writer.Put(new NetSerializer().Serialize(data)); + } + + public static LobbyServerData Deserialize(NetDataReader reader) + { + // Check data available flag + 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 00000000..49902981 --- /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 00000000..3e632ac2 --- /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/LocoResourceModuleData.cs b/Multiplayer/Networking/Data/LocoResourceModuleData.cs new file mode 100644 index 00000000..e750abed --- /dev/null +++ b/Multiplayer/Networking/Data/LocoResourceModuleData.cs @@ -0,0 +1,64 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using System.Linq; + +namespace Multiplayer.Networking.Data; + +public enum LocoResourceModuleFillingState : byte +{ + None = 0, + Filling = 1, + Draining = 2, +} +public readonly struct LocoResourceModuleData(ResourceType resourceType, float[] values, LocoResourceModuleFillingState fillingState) +{ + public readonly ResourceType ResourceType = resourceType; + public readonly float[] Values = values; + public readonly LocoResourceModuleFillingState FillingState = fillingState; + + public static LocoResourceModuleData From(LocoResourceModule resources) + { + //extract floats + var values = resources.resourceData.Select(d => d.unitsToBuy).ToArray(); + + LocoResourceModuleFillingState fillingState = LocoResourceModuleFillingState.None; + if (resources.isFilling) + { + fillingState = LocoResourceModuleFillingState.Filling; + } + else if (resources.isDraining) + { + fillingState = LocoResourceModuleFillingState.Draining; + } + + Multiplayer.LogDebug(() => $"LocoResourceModuleData.From({resources.resourceType}) values count: {values.Length}, values: [{string.Join(", ", values)}]"); + + return new LocoResourceModuleData(resources.resourceType, values, fillingState); + } + + public static void Serialize(NetDataWriter writer, LocoResourceModuleData data) + { + writer.Put((int)data.ResourceType); + + writer.Put(data.Values.Length); + foreach (var val in data.Values) + writer.Put(val); + + writer.Put((byte)data.FillingState); + } + + public static LocoResourceModuleData Deserialize(NetDataReader reader) + { + var type = (ResourceType)reader.GetInt(); + + var valueCount = reader.GetInt(); + + float[] states = new float[valueCount]; + for (int i = 0; i < valueCount; i++) + states[i] = reader.GetFloat(); + + LocoResourceModuleFillingState fillingState = (LocoResourceModuleFillingState)reader.GetByte(); + + return new LocoResourceModuleData(type, states, fillingState); + } +} diff --git a/Multiplayer/Networking/Data/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 323884ed..7f9e74ad 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -1,7 +1,7 @@ +using LiteNetLib.Utils; using System; using System.Collections.Generic; using System.Linq; -using LiteNetLib.Utils; using UnityModManagerNet; namespace Multiplayer.Networking.Data; @@ -11,11 +11,17 @@ public readonly struct ModInfo { public readonly string Id; public readonly string Version; + public readonly string Url; - private ModInfo(string id, string version) + public ModInfo(string id, string version, string url) { Id = id; Version = version; + + if (IsTrustedURL(url)) + Url = url; + else + Url = ""; } public override string ToString() @@ -27,18 +33,79 @@ public static void Serialize(NetDataWriter writer, ModInfo modInfo) { writer.Put(modInfo.Id); writer.Put(modInfo.Version); + writer.Put(modInfo.Url); } public static ModInfo Deserialize(NetDataReader reader) { - return new ModInfo(reader.GetString(), reader.GetString()); + string id = reader.GetString(); + string version = reader.GetString(); + string url = ""; + + if (reader.AvailableBytes > 0) + url = reader.GetString(); + + return new ModInfo(id, version, url); } 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)) + .Select(entry => new ModInfo(entry.Info.Id, entry.Info.Version, entry.Info?.HomePage)) .ToArray(); } + + private static bool IsTrustedURL(string url) + { + if (string.IsNullOrEmpty(url)) + return false; + + if (!Uri.TryCreate(url, UriKind.Absolute, out var uriResult)) + return false; + + var host = uriResult.Host.ToLowerInvariant(); + + if (host == "nexusmods.com" || host == "www.nexusmods.com") + { + Multiplayer.LogDebug(() => $"IsTrustedURL() \"{url}\" is Nexus Mods"); + return true; + } + + if (host == "github.com" || host == "www.github.com") + { + Multiplayer.LogDebug(() => $"IsTrustedURL() \"{url}\" is Github"); + return true; + } + + Multiplayer.LogDebug(() => $"IsTrustedURL() \"{url}\" is untrusted"); + return false; + } + + public static ModInfo[] DeserializeRequiredMods(string json) + { + // Handle null or empty for backward compatibility + if (string.IsNullOrEmpty(json)) + { + Multiplayer.LogWarning("No mod data received (likely from older client/server version)"); + return []; + } + + try + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(json); + } + catch (Exception e) + { + // Try legacy format: comma-separated string of mod names + var modNames = json.Split(',') + .Select(m => m.Trim()) + .Where(m => !string.IsNullOrEmpty(m)) + .Select(m => new ModInfo(m, "Unknown", "")) + .ToArray(); + + return modNames; + } + } } diff --git a/Multiplayer/Networking/Data/PitStopPlugData.cs b/Multiplayer/Networking/Data/PitStopPlugData.cs new file mode 100644 index 00000000..e43da4f7 --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopPlugData.cs @@ -0,0 +1,126 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Networking.Data; + +public readonly struct PitStopPlugData(ushort netId, PlugInteractionType state, byte playerId, ushort trainCarNetId, sbyte socketIndex, Vector3 pos, Quaternion rot) +{ + public readonly ushort NetId = netId; + public readonly byte PlayerId = playerId; + public readonly PlugInteractionType State = state; + public readonly ushort TrainCarNetId = trainCarNetId; + public readonly sbyte SocketIndex = socketIndex; + public readonly Vector3 Position = pos; + public readonly Quaternion Rotation = rot; + + public static PitStopPlugData From(NetworkedPluggableObject plugData, bool bulk = false) + { + var interaction = GetInteractionType(plugData, bulk); + + Multiplayer.LogDebug(() => $"PitStopPlugData.From() NetId: {plugData.NetId}, Interaction: {interaction}"); + return new PitStopPlugData + ( + plugData.NetId, + interaction, + plugData.HeldBy?.PlayerId ?? 0, + plugData.TrainCarNetId, + plugData.SocketIndex, + plugData.transform.GetWorldAbsolutePosition(), + plugData.transform.rotation + ); + } + + public static void Serialize(NetDataWriter writer, PitStopPlugData data) + { + writer.Put(data.NetId); + writer.Put((byte)data.State); + + switch (data.State) + { + case PlugInteractionType.Rejected: + //do nothing?? + break; + case PlugInteractionType.PickedUp: + writer.Put(data.PlayerId); + break; + case PlugInteractionType.Dropped: + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + break; + case PlugInteractionType.DockHome: + //do nothing + break; + case PlugInteractionType.DockSocket: + writer.Put(data.TrainCarNetId); + writer.Put(data.SocketIndex); + break; + } + } + + public static PitStopPlugData Deserialize(NetDataReader reader) + { + ushort netId = reader.GetUShort(); + PlugInteractionType state = (PlugInteractionType)reader.GetByte(); + byte playerId = 0; + ushort trainCarNetId = 0; + sbyte socketIndex = -1; + Vector3 pos = Vector3.zero; + Quaternion rot = Quaternion.identity; + + switch (state) + { + case PlugInteractionType.Rejected: + // No additional data to read + break; + case PlugInteractionType.PickedUp: + playerId = reader.GetByte(); + break; + case PlugInteractionType.Dropped: + pos = Vector3Serializer.Deserialize(reader); + rot = QuaternionSerializer.Deserialize(reader); + break; + case PlugInteractionType.DockHome: + // No additional data to read + break; + case PlugInteractionType.DockSocket: + trainCarNetId = reader.GetUShort(); + socketIndex = reader.GetSByte(); + break; + } + + return new PitStopPlugData + ( + netId, + state, + playerId, + trainCarNetId, + socketIndex, + pos, + rot + ); + } + + private static PlugInteractionType GetInteractionType(NetworkedPluggableObject netPlug, bool bulk) + { + Multiplayer.LogDebug(() => $"NetworkedPluggableObject.GetInteractionType() netId: {netPlug.NetId} bulk: {bulk}, Heldby:{netPlug.HeldBy}, TrainCarNetId: {netPlug.TrainCarNetId} socket not null: {netPlug.PluggableObject.Socket != null}, socket path: {netPlug.PluggableObject.Socket?.GetObjectPath()}, start attached to not null: {netPlug.PluggableObject.startAttachedTo != null}, start attached to path: {netPlug.PluggableObject.startAttachedTo?.GetObjectPath()}"); + //if (!bulk) + // return plugData.CurrentInteraction; + + if (netPlug.HeldBy != null) + return PlugInteractionType.PickedUp; + + if (netPlug.PluggableObject.Socket == null) + return PlugInteractionType.Dropped; + + if (netPlug.PluggableObject.Socket == netPlug.PluggableObject.startAttachedTo) + return PlugInteractionType.DockHome; + + if (netPlug.TrainCarNetId != 0) + return PlugInteractionType.DockSocket; + + return PlugInteractionType.Rejected; + } +} diff --git a/Multiplayer/Networking/Data/PitStopPlugMappingData.cs b/Multiplayer/Networking/Data/PitStopPlugMappingData.cs new file mode 100644 index 00000000..f96192cc --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopPlugMappingData.cs @@ -0,0 +1,56 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Data; + +public readonly struct PitStopPlugMappingData(ushort netId, Dictionary plugMapping) +{ + public readonly ushort NetId = netId; + public readonly Dictionary PlugMapping = plugMapping; + + + public static PitStopPlugMappingData From(NetworkedPitStopStation netStation) + { + var netId = netStation.NetId; + var plugMapping = netStation.GetPluggables(); + + return new PitStopPlugMappingData + ( + netId, + plugMapping + ); + } + + public static void Serialize(NetDataWriter writer, PitStopPlugMappingData data) + { + writer.Put(data.NetId); + + writer.Put(data.PlugMapping.Count); + foreach (var kvp in data.PlugMapping) + { + writer.Put((int)kvp.Key); + writer.Put(kvp.Value); + } + } + + public static PitStopPlugMappingData Deserialize(NetDataReader reader) + { + var netId = reader.GetUShort(); + + var dictCount = reader.GetInt(); + + Dictionary plugMapping = []; + for (int i = 0; i < dictCount; i++) + { + plugMapping.Add((ResourceType)reader.GetInt(), reader.GetUShort()); + } + + return new PitStopPlugMappingData + ( + netId, + plugMapping + ); + } +} diff --git a/Multiplayer/Networking/Data/PitStopStationInteractionType.cs b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs new file mode 100644 index 00000000..bd500642 --- /dev/null +++ b/Multiplayer/Networking/Data/PitStopStationInteractionType.cs @@ -0,0 +1,18 @@ +using System; + +namespace Multiplayer.Networking.Data; + +public enum PitStopStationInteractionType : byte +{ + Reject, + LeverState, + ResourceUpdate, + + CarSelectorGrab, + CarSelectorUngrab, + CarSelection, + + FaucetGrab, + FaucetUngrab, + FaucetPosition, +} diff --git a/Multiplayer/Networking/Data/PlugInteractionType.cs b/Multiplayer/Networking/Data/PlugInteractionType.cs new file mode 100644 index 00000000..db4417d1 --- /dev/null +++ b/Multiplayer/Networking/Data/PlugInteractionType.cs @@ -0,0 +1,13 @@ + +namespace Multiplayer.Networking.Data +{ + public enum PlugInteractionType : byte + { + Rejected, + PickedUp, + Dropped, + Yanked, + DockHome, + DockSocket + } +} diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 4e367e53..cde22259 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -1,33 +1,177 @@ -using System; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.TransportLayers; +using Multiplayer.Utils; +using System.Collections.Generic; +using System; using UnityEngine; namespace Multiplayer.Networking.Data; -public class ServerPlayer +public class ServerPlayer : IDisposable { - public byte Id { get; set; } + #region ID Management + private static readonly IdPool idPool = new(); + + public void Dispose() + { + Multiplayer.LogDebug(() => $"Disposing ServerPlayer {Username} ({PlayerId})"); + if (PlayerId != 0) + { + idPool.ReleaseId(PlayerId); + PlayerId = 0; + } + } + #endregion + + public ITransportPeer Peer { get; private set; } + public byte PlayerId { get; private 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(); + + private Vector3 _lastWorldPos = Vector3.zero; + private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; + + public ServerPlayer(ITransportPeer peer, string username, string originalUsername, Guid guid) + { + PlayerId = idPool.NextId; + + Peer = peer; + + Username = username; + OriginalUsername = originalUsername; + Guid = guid; + } + + #region Positioning + public Vector3 AbsoluteWorldPosition + { + get + { + + Vector3 pos; + try + { + if (CarId == 0 || !NetworkedTrainCar.TryGet(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.TryGet(CarId, out NetworkedTrainCar car)) + { + if(CarId != 0) + Multiplayer.LogDebug(() =>$"WorldPosition() noID {Username}: CarId: {CarId}"); - public Vector3 WorldPosition => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) - ? RawPosition + WorldMover.currentMove - : car.transform.TransformPoint(RawPosition); + pos = RawPosition + WorldMover.currentMove; + } + else + { + //Multiplayer.LogDebug(() => $"WorldPosition() hasID {Username}: CarId: {CarId}"); + pos = car.transform.TransformPoint(RawPosition); + } - public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) + _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.TryGet(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() { - return $"{Id} ({Username}, {Guid.ToString()})"; + return $"{PlayerId} ({Username}, {Guid.ToString()})"; } } diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs new file mode 100644 index 00000000..9ebdf9de --- /dev/null +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -0,0 +1,507 @@ +using DV.Logic.Job; +using DV.ThingTypes; +using MPAPI.Types; +using MPAPI.Util; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; +using DV.ThingTypes.TransitionHelpers; + +namespace Multiplayer.Networking.Data; + +#region Extension of TaskTypes +public static class TaskNetworkDataFactory +{ + private static readonly Dictionary> TypeToTaskNetworkData = []; + private static readonly Dictionary> EnumToEmptyTaskNetworkData = []; + internal static readonly List baseTasks = []; + internal static readonly List baseTaskTypes = []; + + public static bool RegisterTaskType(TaskType taskType) + where TGameTask : Task + where TNetworkData : TaskNetworkData, new() + { + Multiplayer.LogDebug(() => $"Registering Task Type {typeof(TGameTask)} with TaskType {taskType}"); + + if (TypeToTaskNetworkData.Keys.Contains(typeof(TGameTask)) || EnumToEmptyTaskNetworkData.Keys.Contains(taskType)) + { + Multiplayer.LogError($"Task Type {typeof(TGameTask)} already registered!"); + return false; + } + + TypeToTaskNetworkData[typeof(TGameTask)] = task => + { + var networkData = new TNetworkData { TaskType = taskType }; + return ((TaskNetworkData)networkData).FromTask(task); + }; + + EnumToEmptyTaskNetworkData[taskType] = type => new TNetworkData { TaskType = type }; + + return true; + } + + public static bool UnregisterTaskType(TaskType taskType) + where TGameTask : Task + { + Multiplayer.LogDebug(() => $"Unregistering Task Type {typeof(TGameTask)} with TaskType {taskType}"); + if (baseTasks.Contains(typeof(TGameTask)) || baseTaskTypes.Contains(taskType)) + { + Multiplayer.LogError($"Cannot unregister base task type {typeof(TGameTask)} with TaskType {taskType}"); + return false; + } + + TypeToTaskNetworkData.Remove(typeof(TGameTask)); + EnumToEmptyTaskNetworkData.Remove(taskType); + + return true; + } + + public static TaskNetworkData ConvertTask(Task task) + { + Multiplayer.LogDebug(() => $"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.InstanceTaskType}"); + if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) + { + var taskData = converter(task); + + if (NetworkedTask.TryGetNetId(task, out var taskNetId) && taskNetId != 0) + taskData.TaskNetId = taskNetId; + else + Multiplayer.LogError($"TaskNetworkDataFactory.ConvertTask: Could not find NetworkedJob for jobId: {task.Job.ID}, taskType: {task.InstanceTaskType}"); + + return taskData; + } + throw new ArgumentException($"Unknown task type: {task.GetType()}"); + } + + public static TaskNetworkData[] ConvertTasks(IEnumerable tasks) + { + return tasks.Select(ConvertTask).ToArray(); + } + + public static TaskNetworkData ConvertTask(TaskType taskType) + { + //Multiplayer.LogDebug(() => $"TaskNetworkDataFactory.ConvertTask({type})"); + if (EnumToEmptyTaskNetworkData.TryGetValue(taskType, out var creator)) + { + return creator(taskType); + } + throw new ArgumentException($"Unknown task type: {taskType}"); + } + + // Register base task types + static TaskNetworkDataFactory() + { + RegisterTaskType(TaskType.Warehouse); + + baseTasks.Add(typeof(WarehouseTask)); + baseTaskTypes.Add(TaskType.Warehouse); + + RegisterTaskType(TaskType.Transport); + + baseTasks.Add(typeof(TransportTask)); + baseTaskTypes.Add(TaskType.Transport); + + RegisterTaskType(TaskType.Sequential); + + baseTasks.Add(typeof(SequentialTasks)); + baseTaskTypes.Add(TaskType.Sequential); + + RegisterTaskType(TaskType.Parallel); + + baseTasks.Add(typeof(ParallelTasks)); + baseTaskTypes.Add(TaskType.Parallel); + } +} +#endregion + +#region Base Task Types + +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(BinaryWriter writer) + { + SerializeCommon(writer); + writer.WriteUShortArray(CarNetIDs); + writer.Write((byte)WarehouseTaskType); + writer.Write(WarehouseMachine ?? string.Empty); + + if (!CargoTypeLookup.Instance.TryGetNetId(CargoType.ToV2(), out var cargoNetId)) + Multiplayer.LogError($"WarehouseTaskData.Serialize(): Could not find netId for CargoType {CargoType}"); + + writer.Write(cargoNetId); + writer.Write(CargoAmount); + writer.Write(ReadyForMachine); + } + + public override void Deserialize(BinaryReader reader) + { + DeserializeCommon(reader); + CarNetIDs = reader.ReadUShortArray(); + WarehouseTaskType = (WarehouseTaskType)reader.ReadByte(); + WarehouseMachine = reader.ReadString(); + + uint cargoNetId = reader.ReadUInt32(); + CargoTypeLookup.Instance.TryGet(cargoNetId, out CargoType cargoType); + CargoType = cargoType; + + CargoAmount = reader.ReadSingle(); + ReadyForMachine = reader.ReadBoolean(); + } + + public override WarehouseTaskData FromTask(Task task) + { + if (task is not WarehouseTask warehouseTask) + throw new ArgumentException("Task is not a WarehouseTask"); + + FromTaskCommon(task); + + 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(ref Dictionary netIdToTask) + { + List cars = CarNetIDs + .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar : null) + .Where(car => car != null) + .Select(car => car.logicCar) + .ToList(); + + WarehouseTask newWarehouseTask = new + ( + cars, + WarehouseTaskType, + JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), + CargoType, + CargoAmount, + (long)TimeLimit, + IsLastTask + ); + + ToTaskCommon(newWarehouseTask); + + newWarehouseTask.readyForMachine = ReadyForMachine; + + netIdToTask.Add(TaskNetId, newWarehouseTask); + + return newWarehouseTask; + } + + 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(BinaryWriter writer) + { + SerializeCommon(writer); + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + writer.WriteUShortArray(CarNetIDs); + + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw after: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); + + //Multiplayer.Log($"TaskNetworkData.Serialize() StartingTrack {StartingTrack}"); + writer.Write(StartingTrack); + //Multiplayer.Log($"TaskNetworkData.Serialize() DestinationTrack {DestinationTrack}"); + writer.Write(DestinationTrack); + + //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); + + writer.Write(TransportedCargoPerCar?.Length ?? 0); + + if (TransportedCargoPerCar != null) + { + foreach (var cargoType in TransportedCargoPerCar) + { + CargoTypeLookup.Instance.TryGetNetId(cargoType.ToV2(), out var cargoNetId); + writer.Write(cargoNetId); + } + } + + //Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + writer.Write(CouplingRequiredAndNotDone); + //Multiplayer.Log($"TaskNetworkData.Serialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + writer.Write(AnyHandbrakeRequiredAndNotDone); + } + + public override void Deserialize(BinaryReader reader) + { + DeserializeCommon(reader); + + CarNetIDs = reader.ReadUShortArray(); + + //Multiplayer.LogDebug(() => $"TransportTaskData.Deserialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + + StartingTrack = reader.ReadString(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() StartingTrack {StartingTrack}"); + DestinationTrack = reader.ReadString(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); + + var cargoCount = reader.ReadInt32(); + if (cargoCount > 0) + { + TransportedCargoPerCar = new CargoType[cargoCount]; + + for (var i = 0; i < cargoCount; i++) + { + uint cargoNetId = reader.ReadUInt32(); + CargoTypeLookup.Instance.TryGet(cargoNetId, out CargoType cargoType); + TransportedCargoPerCar[i] = cargoType; + } + } + + CouplingRequiredAndNotDone = reader.ReadBoolean(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + AnyHandbrakeRequiredAndNotDone = reader.ReadBoolean(); + //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"); + + FromTaskCommon(task); + + //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(ref Dictionary netIdToTask) + { + List cars = CarNetIDs + .Select(netId => NetworkedTrainCar.TryGet(netId, out TrainCar trainCar) ? trainCar.logicCar : null) + .Where(car => car != null) + .ToList(); + + var newTransportTask = new TransportTask + ( + cars, + RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).LogicTrack(), + RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).LogicTrack(), + TransportedCargoPerCar?.ToList(), + (long)TimeLimit, + IsLastTask + ); + + ToTaskCommon(newTransportTask); + + netIdToTask.Add(TaskNetId, newTransportTask); + + return newTransportTask; + } + + public override List GetCars() + { + return CarNetIDs.ToList(); + } +} + +public class SequentialTasksData : TaskNetworkData +{ + public TaskNetworkData[] Tasks { get; set; } + + + public override void Serialize(BinaryWriter writer) + { + //Multiplayer.Log($"SequentialTasksData.Serialize({writer != null})"); + + SerializeCommon(writer); + + //Multiplayer.Log($"SequentialTasksData.Serialize() {Tasks.Length}"); + + writer.Write((byte)Tasks.Length); + foreach (var task in Tasks) + { + //Multiplayer.Log($"SequentialTasksData.Serialize() {task.TaskType} {task.GetType()}"); + writer.Write((byte)task.TaskType); + task.Serialize(writer); + } + } + + public override void Deserialize(BinaryReader reader) + { + DeserializeCommon(reader); + var tasksLength = reader.ReadByte(); + Tasks = new TaskNetworkData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + { + var taskType = (TaskType)reader.ReadByte(); + Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + Tasks[i].Deserialize(reader); + } + } + + public override SequentialTasksData FromTask(Task task) + { + if (task is not SequentialTasks sequentialTasks) + throw new ArgumentException("Task is not a SequentialTasks"); + + FromTaskCommon(task); + + Tasks = TaskNetworkDataFactory.ConvertTasks(sequentialTasks.tasks); + + return this; + } + + public override Task ToTask(ref Dictionary netIdToTask) + { + List tasks = []; + + foreach (var task in Tasks) + { + var taskResults = task.ToTask(ref netIdToTask); + tasks.Add(taskResults); + } + + SequentialTasks newSequentialTask = new(tasks, (long)TimeLimit); + + ToTaskCommon(newSequentialTask); + + netIdToTask.Add(TaskNetId, newSequentialTask); + + // Rebuild linked list task states - this is the equivalent of OverrideTasksStates(TaskSaveData[] tasksData) + int index = 0; + for (var currentNode = newSequentialTask.tasks.First; currentNode != null; currentNode = currentNode.Next) + { + currentNode.Value.state = tasks[index].state; + currentNode.Value.taskStartTime = tasks[index].taskStartTime; + currentNode.Value.taskFinishTime = tasks[index].taskFinishTime; + + if (tasks[index].state == TaskState.Done && currentNode != newSequentialTask.tasks.Last) + newSequentialTask.currentTask = currentNode.Next; + + index++; + } + + return newSequentialTask; + } + + 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(BinaryWriter writer) + { + SerializeCommon(writer); + writer.Write((byte)Tasks.Length); + foreach (var task in Tasks) + { + writer.Write((byte)task.TaskType); + task.Serialize(writer); + } + } + + public override void Deserialize(BinaryReader reader) + { + DeserializeCommon(reader); + var tasksLength = reader.ReadByte(); + Tasks = new TaskNetworkData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + { + var taskType = (TaskType)reader.ReadByte(); + 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"); + + FromTaskCommon(task); + + Tasks = TaskNetworkDataFactory.ConvertTasks(parallelTasks.tasks); + + return this; + } + + public override Task ToTask(ref Dictionary netIdToTask) + { + List taskList = new(Tasks.Length); + + for (int i = 0; i < Tasks.Length; i++) + taskList.Add(Tasks[i].ToTask(ref netIdToTask)); + + var newParallelTasks = new ParallelTasks(taskList, (long)TimeLimit, IsLastTask); + + ToTaskCommon(newParallelTasks); + + netIdToTask.Add(TaskNetId, newParallelTasks); + + return newParallelTasks; + } + + public override List GetCars() + { + List result = []; + + foreach (var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } +} +#endregion diff --git a/Multiplayer/Networking/Data/TrackedValue.cs b/Multiplayer/Networking/Data/TrackedValue.cs new file mode 100644 index 00000000..02db9d31 --- /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 00000000..10969d65 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/BogieData.cs @@ -0,0 +1,80 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; +using System; + +namespace Multiplayer.Networking.Data.Train; + +[Flags] +public enum BogieFlags : byte +{ + None = 0, + IncludesTrackData = 1, + HasDerailed = 2, + TrackReversed = 4 +} +public readonly struct BogieData +{ + public readonly BogieFlags DataFlags; + public readonly double PositionAlongTrack; + public readonly ushort TrackNetId; + + public readonly int TrackDirection => DataFlags.HasFlag(BogieFlags.TrackReversed) ? -1 : 1; + public readonly bool IncludesTrackData => DataFlags.HasFlag(BogieFlags.IncludesTrackData); + public readonly bool HasDerailed => DataFlags.HasFlag(BogieFlags.HasDerailed); + + private BogieData(BogieFlags flags, double positionAlongTrack, ushort trackNetId) + { + // Prevent invalid state combinations + if (flags.HasFlag(BogieFlags.HasDerailed)) + flags &= ~BogieFlags.IncludesTrackData; // Clear track data flag if derailed + + DataFlags = flags; + PositionAlongTrack = positionAlongTrack; + TrackNetId = trackNetId; + } + + public static BogieData FromBogie(Bogie bogie) + { + bool includesTrackData = !bogie.HasDerailed && bogie.track; + + BogieFlags flags = BogieFlags.None; + + if (includesTrackData) flags |= BogieFlags.IncludesTrackData; + if (bogie.HasDerailed) flags |= BogieFlags.HasDerailed; + if (bogie.trackDirection == -1) flags |= BogieFlags.TrackReversed; + + return new BogieData( + flags, + bogie.traveller?.Span ?? -1.0, + includesTrackData ? bogie.track.Networked().NetId : (ushort)0 + ); + } + + public static void Serialize(NetDataWriter writer, BogieData data) + { + writer.Put((byte)data.DataFlags); + + if (!data.HasDerailed) + writer.Put(data.PositionAlongTrack); + + if (data.IncludesTrackData) + writer.Put(data.TrackNetId); + } + + 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; + if (flags.HasFlag(BogieFlags.IncludesTrackData)) + trackNetId = reader.GetUShort(); + + return new BogieData(flags, positionAlongTrack, trackNetId); + } +} diff --git a/Multiplayer/Networking/Data/Train/BrakeSystemData.cs b/Multiplayer/Networking/Data/Train/BrakeSystemData.cs new file mode 100644 index 00000000..2fef7bbc --- /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 00000000..36845ad2 --- /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 00000000..3424cb3e --- /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 d4161d80..00af0e74 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/TrainCarHealthData.cs b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs new file mode 100644 index 00000000..83a05d33 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainCarHealthData.cs @@ -0,0 +1,95 @@ +using DV.Damage; +using LiteNetLib.Utils; +using Multiplayer.Utils; +using System; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct TrainCarHealthData +{ + public readonly float BodyHP; + public readonly float WheelsHP; + public readonly float MechanicalPT; + public readonly float ElectricalPT; + public readonly bool WindowsBroken; + + private TrainCarHealthData(float bodyHP, float wheelsHP, float mechanicalPT, float electricalPT, bool windowsBroken) + { + BodyHP = bodyHP; + WheelsHP = wheelsHP; + MechanicalPT = mechanicalPT; + ElectricalPT = electricalPT; + WindowsBroken = windowsBroken; + } + public void LoadTo(TrainCar trainCar) + { + //Multiplayer.LogDebug(() => $"TrainCarHealthData.LoadTo([{trainCar.ID}, {trainCar.GetNetId()}])"); + var dmgCtrl = trainCar.GetComponent(); + if (dmgCtrl != null) + { + //Multiplayer.LogDebug(() => $"TrainCarHealthData.LoadTo([{trainCar.ID}, {trainCar.GetNetId()}])\r\n" + + //$"Damage Controller: Body: {dmgCtrl?.bodyDamage?.HealthPercentage}, " + + //$"Wheels: {dmgCtrl?.wheels?.HealthPercentage}, " + + //$"Mechanical: {dmgCtrl?.mechanicalPT?.HealthPercentage}, " + + //$"Electrical: {dmgCtrl?.electricalPT?.HealthPercentage}, " + + //$"Windows: {dmgCtrl?.windows?.windowsBroken}"); + + dmgCtrl.bodyDamage.LoadCarDamageState(BodyHP); + dmgCtrl.wheels?.SetCurrentHealthPercentage(WheelsHP); + dmgCtrl.mechanicalPT?.SetCurrentHealthPercentage(MechanicalPT); + dmgCtrl.electricalPT?.SetCurrentHealthPercentage(ElectricalPT); + + if (dmgCtrl.windows != null) + dmgCtrl.windows.windowsBroken = WindowsBroken; + + return; + } + + var dmgModel = trainCar.GetComponent(); + //Multiplayer.LogDebug(() => $"TrainCarHealthData.LoadTo([{trainCar.ID}, {trainCar.GetNetId()}]) Using CarDamageModel: {dmgModel !=null}"); + dmgModel?.SetHealth(BodyHP); + } + + public static TrainCarHealthData From(TrainCar trainCar) + { + var dmgCtrl = trainCar.GetComponent(); + + if (dmgCtrl == null) + { + //freight cars don't have damage controller, so we need to check if they have a damage model + var dmgModel = trainCar.GetComponent(); + if (dmgModel != null) + return new TrainCarHealthData(dmgModel.currentHealth, 0,0,0,false); + else + return new TrainCarHealthData(); + } + + float bodyHP = dmgCtrl?.bodyDamage?.HealthPercentage ?? 0; + float wheelsHP = dmgCtrl?.wheels?.HealthPercentage ?? 0; + float mechanicalPT = dmgCtrl?.mechanicalPT?.HealthPercentage ?? 0; + float electricalPT = dmgCtrl?.electricalPT?.HealthPercentage ?? 0; + bool brokenWindows = dmgCtrl?.windows?.windowsBroken ?? true; + + return new TrainCarHealthData(bodyHP, wheelsHP, mechanicalPT, electricalPT, brokenWindows); + } + + public static void Serialize(NetDataWriter writer, TrainCarHealthData data) + { + writer.Put(data.BodyHP); + writer.Put(data.WheelsHP); + writer.Put(data.MechanicalPT); + writer.Put(data.ElectricalPT); + writer.Put(data.WindowsBroken); + } + + public static TrainCarHealthData Deserialize(NetDataReader reader) + { + float bodyHP = reader.GetFloat(); + float wheelsHP = reader.GetFloat(); + float mechanicalPT = reader.GetFloat(); + float electricalPT = reader.GetFloat(); + bool brokenWindows = reader.GetBool(); + + return new TrainCarHealthData(bodyHP, wheelsHP, mechanicalPT, electricalPT, brokenWindows); + } +} diff --git a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs new file mode 100644 index 00000000..2acc8d81 --- /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; + 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(netId, 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(netId, 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 00000000..e713bf04 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -0,0 +1,225 @@ +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; + public readonly bool Exploded; + public readonly TrainCarHealthData CarHealthData; + + // 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 exploded, TrainCarHealthData carHealthData, + 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; + Exploded = exploded; + CarHealthData = carHealthData; + + 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.Exploded); + TrainCarHealthData.Serialize(writer, data.CarHealthData); + + writer.Put(data.PlayerSpawnedCar); + writer.Put(data.IsRestorationLoco); + + if(data.IsRestorationLoco) + writer.Put((byte) data.RestorationState); + + PaintThemeLookup.Instance.TryGetNetId(data.PaintExterior, out var extPaintNetId); + writer.Put(extPaintNetId); + + PaintThemeLookup.Instance.TryGetNetId(data.PaintInterior, out var intPaintNetId); + writer.Put(intPaintNetId); + + + 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 exploded = reader.GetBool(); + TrainCarHealthData healthData = TrainCarHealthData.Deserialize(reader); + + bool playerSpawnedCar = reader.GetBool(); + bool isRestoration = reader.GetBool(); + LocoRestorationController.RestorationState restorationState = default; + if (isRestoration) + restorationState = (LocoRestorationController.RestorationState)reader.GetByte(); + + uint extThemeId = reader.GetUInt(); + uint intThemeId = reader.GetUInt(); + + + PaintThemeLookup.Instance.TryGet(extThemeId, out PaintTheme exteriorPaint); + PaintThemeLookup.Instance.TryGet(intThemeId, out PaintTheme interiorPaint); + + 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, exploded, healthData, + 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.isExploded, + TrainCarHealthData.From(trainCar), + + 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]), + BogieData.FromBogie(trainCar.Bogies[1]), + 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 62fade00..00000000 --- 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 5d6b6cdb..00000000 --- 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/Data/WarehouseData.cs b/Multiplayer/Networking/Data/WarehouseData.cs new file mode 100644 index 00000000..22e649e9 --- /dev/null +++ b/Multiplayer/Networking/Data/WarehouseData.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Data; + +public enum WarehouseAction : byte +{ + Load, + Unload +} diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index 1911f9c1..0728d8ad 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -1,18 +1,18 @@ -using System; -using System.Collections.Generic; using DV; using Multiplayer.Components.Networking.Player; +using System.Collections.Generic; +using System; using UnityEngine; using Object = UnityEngine.Object; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Client; public class ClientPlayerManager { private readonly Dictionary playerMap = new(); - public Action OnPlayerConnected; - public Action OnPlayerDisconnected; + public Action OnPlayerConnected; + public Action OnPlayerDisconnected; public IReadOnlyCollection Players => playerMap.Values; private readonly GameObject playerPrefab; @@ -22,50 +22,52 @@ public ClientPlayerManager() playerPrefab = Multiplayer.AssetIndex.playerPrefab; } - public bool TryGetPlayer(byte id, out NetworkedPlayer player) + public bool TryGetPlayer(byte playerid, out NetworkedPlayer player) { - return playerMap.TryGetValue(id, out player); + return playerMap.TryGetValue(playerid, out player); } - public void AddPlayer(byte id, string username, Guid guid) + public void AddPlayer(byte playerId, string username) { - GameObject go = Object.Instantiate(playerPrefab, WorldMover.Instance.originShiftParent); + GameObject go = Object.Instantiate(playerPrefab, WorldMover.OriginShiftParent); go.layer = LayerMask.NameToLayer(Layers.Player); NetworkedPlayer networkedPlayer = go.AddComponent(); - networkedPlayer.Id = id; + networkedPlayer.PlayerId = playerId; networkedPlayer.Username = username; - networkedPlayer.Guid = guid; - playerMap.Add(id, networkedPlayer); - OnPlayerConnected?.Invoke(id, networkedPlayer); + //networkedPlayer.Guid = guid; + playerMap.Add(playerId, networkedPlayer); + OnPlayerConnected?.Invoke(networkedPlayer); } - public void RemovePlayer(byte id) + public void RemovePlayer(byte playerid) { - if (!playerMap.TryGetValue(id, out NetworkedPlayer networkedPlayer)) + if (!TryGetPlayer(playerid, out NetworkedPlayer networkedPlayer)) return; - OnPlayerDisconnected?.Invoke(id, networkedPlayer); + + OnPlayerDisconnected?.Invoke(networkedPlayer); Object.Destroy(networkedPlayer.gameObject); - playerMap.Remove(id); + playerMap.Remove(playerid); } - public void UpdatePing(byte id, int ping) + public void UpdatePing(byte playerId, int ping) { - if (!playerMap.TryGetValue(id, out NetworkedPlayer player)) + if (!TryGetPlayer(playerId, out NetworkedPlayer player)) return; player.SetPing(ping); } - public void UpdatePosition(byte id, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar) + public void UpdatePosition(byte playerid, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar, ushort carId) { - if (!playerMap.TryGetValue(id, out NetworkedPlayer player)) + if (!TryGetPlayer(playerid, 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 b44d387d..c07b2f08 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,6 +1,6 @@ -using System; -using System.Text; using DV; +using DV.Common; +using DV.Customization.Paint; using DV.Damage; using DV.InventorySystem; using DV.Logic.Job; @@ -8,85 +8,151 @@ using DV.ServicePenalty.UI; using DV.ThingTypes; using DV.UI; -using DV.UIFramework; +using DV.UserManagement; using DV.WeatherSystem; using LiteNetLib; +using LiteNetLib.Utils; +using MPAPI.Interfaces.Packets; +using MPAPI.Types; +using Multiplayer.API; 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.Data.Train; 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.Packets.Serverbound.Jobs; +using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.TransportLayers; +using Multiplayer.Patches.MainMenu; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using UnityEngine; -using UnityModManagerNet; using Object = UnityEngine.Object; -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; + + private ITransportPeer selfPeer; + public byte PlayerId { get; private set; } + public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds public int Ping { get; private set; } - private NetPeer serverPeer; + private ITransportPeer serverPeer; + public float RPC_Timeout => (Ping * 4f) / 1000; + + private ChatGUI chatGUI; + private readonly bool isSinglePlayer; - public NetworkClient(Settings settings) : base(settings) + private bool isAlsoHost; + IGameSession originalSession; + + // Allow mods to add to the wait Queue + private readonly List readyBlocks = []; + + public NetworkClient(Settings settings, bool singlePlayer) : base(settings) { - PlayerManager = new ClientPlayerManager(); + isSinglePlayer = singlePlayer; + ClientPlayerManager = new ClientPlayerManager(); + + WorldStreamingInit.LoadingFinished += () => + { + NetworkedPlayer.CaptureItemAnchorOffset(); + }; } - 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) + BuildVersion = MainMenuControllerPatch.MenuProvider.BuildVersionString, + Mods = ModCompatibilityManager.Instance.GetLocalMods() }; 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(OnClientboundPlayerJoinedPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPlayerCarPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); - netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); + netPacketProcessor.SubscribeReusable(OnClientboundLoginResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundDisconnectPacket); + netPacketProcessor.SubscribeReusable(OnClientboundRemoveLoadingScreen); + + netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundBeginWorldSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundGameParamsPacket); netPacketProcessor.SubscribeReusable(OnClientboundSaveGameDataPacket); netPacketProcessor.SubscribeReusable(OnClientboundWeatherPacket); - netPacketProcessor.SubscribeReusable(OnClientboundRemoveLoadingScreen); - netPacketProcessor.SubscribeReusable(OnClientboundTimeAdvancePacket); netPacketProcessor.SubscribeReusable(OnClientboundRailwayStatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundStationControllerLookupPacket); + + + netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); + + netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); + + netPacketProcessor.SubscribeReusable(OnClientboundTimeAdvancePacket); netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainSetPacket); netPacketProcessor.SubscribeReusable(OnClientboundDestroyTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundTrainPhysicsPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); @@ -95,10 +161,17 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnClientboundTrainControlAuthorityUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundCargoHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundWarehouseControllerUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundRerailTrainPacket); netPacketProcessor.SubscribeReusable(OnClientboundWindowsBrokenPacket); netPacketProcessor.SubscribeReusable(OnClientboundWindowsRepairedPacket); @@ -106,28 +179,116 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundTaskUpdatePacket); + + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); + + netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonPitStopPlugInteractionPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPitStopBulkUpdatePacket); + + netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); + + netPacketProcessor.SubscribeReusable(OnCommonGenericSwitchStatePacket); + + } + + // Allow mods to register their own packets + public void RegisterExternalPacket(ClientPacketHandler handler) where T : class, IPacket, new() + { + netPacketProcessor.SubscribeReusable((packet) => + { + handler(packet); + }); + } + + public void RegisterExternalSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new() + { + netPacketProcessor.SubscribeNetSerializable>((wrapper) => + { + handler(wrapper.Packet); + }, + () => new ExternalSerializablePacketWrapper() + ); + } + + // Allow mods to register ready blocks + internal void RegisterReadyBlock(string modName) + { + Log($"Ready Block has been registered by {modName}"); + + if (readyBlocks.Contains(modName)) + return; + + readyBlocks.Add(modName); + } + + internal void CancelReadyBlock(string modName) + { + Log($"Ready Block has been cleared by {modName}"); + + if (readyBlocks.Contains(modName)) + { + readyBlocks.Remove(modName); + DisplayLoadingInfo displayLoadingInfo = Object.FindObjectOfType(); + displayLoadingInfo?.OnLoadingStatusChanged($"Mod {modName} loaded", false, 100); + } + } + + private void OnLoaded() + { + Log($"WorldStreamingInit.LoadingFinished()"); + NetworkedItemManager.Instance.CheckInstance(); + Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); + NetworkedItemManager.Instance.CacheWorldItems(); + Log($"WorldStreamingInit.LoadingFinished() InitialiseCashRegisters()"); + NetworkedCashRegisterWithModules.InitialiseCashRegisters(); + Log($"WorldStreamingInit.LoadingFinished() InitialisePitStops()"); + NetworkedPitStopStation.InitialisePitStops(); + Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); + CoroutineManager.Instance.StartCoroutine(WaitForReadyBlocks()); + + WorldStreamingInit.LoadingFinished -= OnLoaded; + } + + private IEnumerator WaitForReadyBlocks() + { + DisplayLoadingInfo displayLoadingInfo = Object.FindObjectOfType(); + foreach (string modName in readyBlocks) + displayLoadingInfo?.OnLoadingStatusChanged($"Waiting for mod {modName} to load", false, 100); + + while (readyBlocks.Count > 0) + { + yield return null; + } + + SendReadyPacket(); } #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 +296,105 @@ 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; + + if (latency > LATENCY_FLAG) + LogWarning($"High Ping Detected! {latency}ms"); } - 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) + 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"); + PlayerId = packet.PlayerId; + + if (NetworkLifecycle.Instance.IsHost()) + SendReadyPacket(); + else + SendSaveGameDataRequest(); + + 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.Missing.Length != 0 || packet.Extra.Length != 0) + if (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"; - } - - if (packet.Extra.Length != 0) - text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); + text += "\n"; + text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); } + } - popup.labelTMPro.text = text; - }); + 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.PlayerId, packet.Username); + + ClientPlayerManager.UpdatePosition(packet.PlayerId, 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.PlayerId}"); + ClientPlayerManager.RemovePlayer(packet.PlayerId); } - 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 + { + Log($"Server Shutting Down"); + disconnectMessage = "Server Shutting Down"; + } } - private void OnClientboundPlayerCarPacket(ClientboundPlayerCarPacket packet) + private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { - PlayerManager.UpdateCar(packet.Id, packet.CarId); + ClientPlayerManager.UpdatePosition(packet.PlayerId, 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.PlayerId, packet.Ping); } private void OnClientboundTickSyncPacket(ClientboundTickSyncPacket packet) @@ -249,8 +419,8 @@ private void OnClientboundServerLoadingPacket(ClientboundServerLoadingPacket pac private void OnClientboundGameParamsPacket(ClientboundGameParamsPacket packet) { LogDebug(() => $"Received {nameof(ClientboundGameParamsPacket)} ({packet.SerializedGameParams.Length} chars)"); - if (Globals.G.gameParams != null) - packet.Apply(Globals.G.gameParams); + if (Globals.G.GameParams != null) + packet.Apply(Globals.G.GameParams); if (Globals.G.gameParamsInstance != null) packet.Apply(Globals.G.gameParamsInstance); } @@ -268,13 +438,18 @@ 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 += OnLoaded; TrainStress.globalIgnoreStressCalculation = true; + } private void OnClientboundBeginWorldSyncPacket(ClientboundBeginWorldSyncPacket packet) @@ -293,7 +468,7 @@ private void OnClientboundBeginWorldSyncPacket(ClientboundBeginWorldSyncPacket p private void OnClientboundWeatherPacket(ClientboundWeatherPacket packet) { - WeatherDriver.Instance.LoadSaveData(JObject.FromObject(packet)); + WeatherDriver.Instance.LoadSaveData(JObject.FromObject(packet), Globals.G.GameParams.WeatherEditorAlwaysAllowed); } private void OnClientboundRemoveLoadingScreen(ClientboundRemoveLoadingScreenPacket packet) @@ -308,6 +483,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,20 +503,53 @@ 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++) { if (!NetworkedJunction.Get((ushort)(i + 1), out NetworkedJunction junction)) return; - junction.Switch((byte)Junction.SwitchMode.NO_SOUND, packet.SelectedJunctionBranches[i]); + junction.Switch((byte)Junction.SwitchMode.NO_SOUND, packet.SelectedJunctionBranches[i], true); } for (int i = 0; i < packet.TurntableRotations.Length; i++) { if (!NetworkedTurntable.Get((byte)(i + 1), out NetworkedTurntable turntable)) return; - turntable.SetRotation(packet.TurntableRotations[i], true); + turntable.SetRotation(packet.TurntableRotations[i], true, true); } } @@ -359,70 +580,162 @@ 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) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + { + LogWarning($"Received DestroyTrainCarPacket for netId: {packet.NetId}, but NetworkedTrainCar was not found."); return; + } + + Log($"Received DestroyTrainCarPacket for [{netTrainCar.CurrentID} {packet.NetId}]"); + + //Protect myself from getting deleted in race conditions + if (PlayerManager.Car == netTrainCar.TrainCar) + { + LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car?.ID}, netId: {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 (netTrainCar == null || netTrainCar.gameObject == null || netTrainCar.TrainCar == null) + { + LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {netTrainCar != null}, trainCar: {netTrainCar?.TrainCar != null}"); + } + else + { + NetworkedPlayer[] componentsInChildren = (netTrainCar?.gameObject != null) ? netTrainCar.GetComponentsInChildren() : []; + + foreach (NetworkedPlayer networkedPlayer in componentsInChildren) + { + networkedPlayer.UpdateCar(0); + } - CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); + netTrainCar.TrainCar.UpdateJobIdOnCarPlates(string.Empty); + CarSpawner.Instance.DeleteCar(netTrainCar.TrainCar); + } } public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket packet) { + //LogDebug(() => $"Received Physics packet for netId: {packet.FirstNetId}, tick: {packet.Tick}"); 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.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + { + LogError($"OnCommonCouplerInteractionPacket netId: {packet.NetId}, TrainCar not found!"); return; + } - Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - - coupler.CoupleTo(otherCoupler, packet.PlayAudio, packet.ViaChainInteraction); + netTrainCar.Common_ReceiveCouplerInteraction(packet); } + //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) + //{ + // TrainCar trainCar = null; + // TrainCar otherTrainCar = null; + + // if (!NetworkedTrainCar.TryGet(packet.NetId, out trainCar) || !NetworkedTrainCar.TryGet(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; + + // 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)) + if (!NetworkedTrainCar.TryGet(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.TryGet(packet.NetId, out TrainCar trainCar); + bool foundOtherTrainCar = NetworkedTrainCar.TryGet(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.TryGet(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); @@ -430,18 +743,18 @@ private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.TryGet(packet.OtherNetId, out TrainCar otherTrainCar)) 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); } private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; MultipleUnitCable cable = packet.IsFront ? trainCar.muModule.frontCable : trainCar.muModule.rearCable; @@ -451,7 +764,7 @@ private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet) private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; @@ -461,7 +774,7 @@ private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet) private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; trainCar.brakeSystem.ReleaseBrakeCylinderPressure(); @@ -469,7 +782,7 @@ private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; trainCar.brakeSystem.SetHandbrakePosition(packet.Position); @@ -477,7 +790,7 @@ private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packe private void OnCommonSimFlowPacket(CommonTrainPortsPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; networkedTrainCar.Common_UpdatePorts(packet); @@ -485,61 +798,164 @@ private void OnCommonSimFlowPacket(CommonTrainPortsPacket packet) private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; networkedTrainCar.Common_UpdateFuses(packet); } + private void OnClientboundTrainControlAuthorityUpdatePacket(ClientboundTrainControlAuthorityUpdatePacket packet) + { + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + networkedTrainCar.Client_ReceiveAuthorityUpdate(packet.PortNetId, packet.State); + } + + private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePacket packet) + { + if (!NetworkedTrainCar.TryGet(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 OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) { - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; + LogDebug(() => $"OnClientboundCargoStatePacket() {networkedTrainCar.CurrentID}, IsLoading: {packet.IsLoading}, CargoType: {packet.CargoTypeNetId}, CargoAmount: {packet.CargoAmount}, Health: {packet.CargoHealth}, CargoModelIndex: {packet.CargoModelIndex}, WarehouseMachineId: {packet.WarehouseMachineNetId}"); + networkedTrainCar.CargoModelIndex = packet.CargoModelIndex; Car logicCar = networkedTrainCar.TrainCar.logicCar; - if (packet.CargoType == (ushort)CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) + if (logicCar == null) + { + LogWarning($"OnClientboundCargoStatePacket() Failed to find logic car for [{networkedTrainCar.TrainCar.ID}, {packet.NetId}] is initialised: {networkedTrainCar.Client_Initialized}"); + return; + } + + CargoTypeLookup.Instance.TryGet(packet.CargoTypeNetId, out CargoType cargoType); + + if (cargoType == 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); + WarehouseMachine warehouseMachine = null; + if (packet.WarehouseMachineNetId != 0 && (!WarehouseMachineLookup.TryGet(packet.WarehouseMachineNetId, out warehouseMachine) || warehouseMachine == null)) + { + LogWarning($"OnClientboundCargoStatePacket() Failed to find WarehouseMachine for netId {packet.WarehouseMachineNetId}"); + return; + } + if (packet.IsLoading) - logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + { + LogDebug(() => $"OnClientboundCargoStatePacket() Loading cargo: {cargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); + //Check correct cargo is loaded and the amount is correct + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == 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) + logicCar.DumpCargo(); + + //We have the correct cargo, but not the right amount, calculate the delta + if (logicCar.CurrentCargoTypeInCar == cargoType) + cargoAmount -= logicCar.LoadedCargoAmount; + + if (cargoAmount > 0) + { + logicCar.LoadCargo(cargoAmount, cargoType, warehouseMachine); + } + + networkedTrainCar.TrainCar.CargoDamage.LoadCargoDamageState(packet.CargoHealth); + } else - logicCar.UnloadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + { + LogDebug(() => $"OnClientboundCargoStatePacket() Unloading cargo: {cargoType} into {networkedTrainCar.CurrentID}, current amount: {packet.CargoAmount}"); + + //Check correct cargo is loaded and the amount is correct + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == 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) + { + //avoid triggering the load event by backdooring it + logicCar.LastUnloadedCargoType = logicCar.CurrentCargoTypeInCar; + logicCar.CurrentCargoTypeInCar = cargoType; + logicCar.LoadedCargoAmount = cargoAmount; + } + + //We have the correct cargo, calculate the delta + if (logicCar.CurrentCargoTypeInCar == cargoType) + cargoAmount = logicCar.LoadedCargoAmount - cargoAmount; + + if (cargoAmount > 0) + logicCar.UnloadCargo(cargoAmount, cargoType, warehouseMachine); + } + } + + private void OnClientboundCargoHealthUpdatePacket(ClientboundCargoHealthUpdatePacket packet) + { + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + CargoDamageModel cargoDamageModel = networkedTrainCar.TrainCar.CargoDamage; + + if (networkedTrainCar.TrainCar == null || cargoDamageModel == null) + return; + + float deltaHealth = cargoDamageModel.currentHealth - packet.CargoHealth; + + //LogDebug(() => $"OnClientboundCargoHealthUpdatePacket() {networkedTrainCar.CurrentID}, current health: {cargoDamageModel.currentHealth}, new health: {packet.CargoHealth}, delta: {cargoDamageModel}, applySensitivity: {packet.CargoHealth > 0}"); + + if (deltaHealth > 0) + cargoDamageModel.ApplyDamageToCargo(deltaHealth, packet.CargoHealth > 0); } private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; - CarDamageModel carDamage = trainCar.CarDamage; - float difference = Mathf.Abs(packet.Health - carDamage.currentHealth); - if (difference < 0.0001) + packet.Health.LoadTo(trainCar); + } + + private void OnClientboundWarehouseControllerUpdatePacket(ClientboundWarehouseControllerUpdatePacket packet) + { + LogDebug(() => $"OnClientboundWarehouseControllerUpdatePacket() NetId: {packet.NetId}, IsLoading: {packet.IsLoading}, JobNetId: {packet.JobNetId}, CarNetId: {packet.CarNetId}, CargoType: {packet.CargoTypeNetId}, Preset: [{(WarehouseMachineController.TextPreset)packet.Preset}, {packet.Preset}]"); + if (!NetworkedWarehouseMachineController.Get(packet.NetId, out NetworkedWarehouseMachineController networkedWarehouseMachineController)) + { + LogWarning($"OnClientboundWarehouseControllerUpdatePacket() Failed to find networked warehouse machine controller for [{packet.NetId}]"); return; + } - if (packet.Health < carDamage.currentHealth) - carDamage.DamageCar(difference); - else - carDamage.RepairCar(difference); + networkedWarehouseMachineController.ClientProcessUpdate(packet); } private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; - if (!NetworkedRailTrack.Get(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) + if (!NetworkedRailTrack.TryGet(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); } private void OnClientboundWindowsBrokenPacket(ClientboundWindowsBrokenPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; DamageController damageController = trainCar.GetComponent(); if (damageController == null) @@ -552,7 +968,7 @@ private void OnClientboundWindowsBrokenPacket(ClientboundWindowsBrokenPacket pac private void OnClientboundWindowsRepairedPacket(ClientboundWindowsRepairedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out TrainCar trainCar)) return; DamageController damageController = trainCar.GetComponent(); if (damageController == null) @@ -579,7 +995,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) @@ -593,6 +1009,190 @@ 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 OnClientboundTaskUpdatePacket(ClientboundTaskUpdatePacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedTask.TryGet(packet.TaskNetId, out Task task) || task == null) + { + LogError($"Received task update for taskNetId {packet.TaskNetId}, task was not found"); + return; + } + + task.SetState(packet.NewState); + task.taskStartTime = packet.TaskStartTime; + task.taskFinishTime = packet.TaskFinishTime; + } + + 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 OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket packet) + { + if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) + { + LogWarning($"Pit Stop Interaction received for netId: {packet.NetId}, but pit stop does not exist!"); + } + + Log($"Pit stop interaction received for {netPitStop.StationName}"); + + LogDebug(() => $"OnCommonPitStopInteractionPacket() [{netPitStop.StationName}, {packet.NetId}], interaction: [{packet.InteractionType}], resource: {packet?.ResourceType}, State: {packet.Value}"); + netPitStop.ProcessInteractionPacketAsClient(packet); + } + + private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet) + { + if (!NetworkedPluggableObject.Get(packet.NetId, out var netPlug)) + { + LogWarning($"Pit Stop Plug Interaction received for plug netId: {packet.NetId}, but pit stop plug does not exist!"); + return; + } + + Log($"Pit Stop Plug Interaction received for {netPlug.NetId}"); + + LogDebug(() => $"OnCommonPitStopPlugInteractionPacket() [{netPlug?.transform?.name}, {packet.NetId}], interaction: [{(PlugInteractionType)packet.InteractionType}]"); + netPlug.ProcessPacket(packet); + } + + private void OnClientboundPitStopBulkUpdatePacket(ClientboundPitStopBulkUpdatePacket packet) + { + LogDebug(() => $"OnClientboundPitStopBulkUpdatePacket() NetId: {packet.NetId}, CarCount: {packet.CarCount}, CarSelection: {packet.CarSelection}, FaucetNotch: {packet.FaucetNotch}, ResourceData Count: {packet.ResourceData.Length}, PlugData: {packet.PlugData.Length}"); + + if (!NetworkedPitStopStation.Get(packet.NetId, out var netPitStop)) + { + LogWarning($"Pit Stop Bulk Data received for station netId: {packet.NetId}, but pit stop does not exist!"); + return; + } + + Log($"Pit Stop Bulk Data received for {netPitStop.StationName}"); + + netPitStop.ProcessBulkUpdate(packet); + } + + + private void OnCommonItemChangePacket(CommonItemChangePacket packet) + { + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); + + + //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.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + return; + + if (!PaintThemeLookup.Instance.TryGet(packet.PaintThemeId, out PaintTheme paint) || paint == null) + { + LogWarning($"Received paint theme change for {netTrainCar?.CurrentID}, but paint theme id '{packet.PaintThemeId}' does not exist."); + return; + } + + Log($"Received paint theme change for {netTrainCar?.CurrentID}, theme '{paint.AssetName}'"); + + LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {packet.TargetArea}, paint: [{paint?.AssetName}, {packet.PaintThemeId}]"); + netTrainCar?.Common_ReceivePaintThemeUpdate(packet.TargetArea, paint); + } + + private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithModulesActionPacket packet) + { + if (!NetworkedCashRegisterWithModules.Get(packet.NetId, out NetworkedCashRegisterWithModules netCashRegister)) + { + LogWarning($"Cash Register With Modules Action received for netId: {packet.NetId}, but cash register does not exist!"); + return; + } + + Log($"Cash Register With Modules Action received for {netCashRegister.GetObjectPath()}, Action: {packet.Action}, Amount: {packet.Amount}"); + + netCashRegister.Client_ProcessCashRegisterAction(packet.Action, packet.Amount); + } + + private void OnCommonGenericSwitchStatePacket(CommonGenericSwitchStatePacket packet) + { + if (!NetworkedGenericSwitch.TryGet(packet.NetId, out NetworkedGenericSwitch netSwitch)) + { + LogWarning($"Received Generic Switch State for switch {packet.NetId}, but switch does not exist!"); + return; + } + + netSwitch.Client_ReceiveSwitchState(packet.IsOn); + } + #endregion #region Senders @@ -602,6 +1202,26 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) SendPacket(serverPeer, packet, deliveryMethod); } + private void SendNetSerializablePacketToServer(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + SendNetSerializablePacket(serverPeer, packet, deliveryMethod); + } + + + #region Mod Packets + public void SendExternalPacketToServer(T packet, bool reliable) where T : class, IPacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacketToServer(packet, deliveryMethod); + } + + public void SendExternalSerializablePacketToServer(T packet, bool reliable) where T : class, ISerializablePacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacketToServer(wrapper, deliveryMethod); + } + #endregion public void SendSaveGameDataRequest() { SendPacketToServer(new ServerboundSaveGameDataRequestPacket(), DeliveryMethod.ReliableOrdered); @@ -613,169 +1233,277 @@ 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); + }, DeliveryMethod.ReliableOrdered); } public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.SwitchMode mode) { - SendPacketToServer(new CommonChangeJunctionPacket { + SendPacketToServer(new CommonChangeJunctionPacket + { NetId = netId, SelectedBranch = selectedBranch, Mode = (byte)mode - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendTurntableRotation(byte netId, float rotation) { - SendPacketToServer(new CommonRotateTurntablePacket { + SendPacketToServer(new CommonRotateTurntablePacket + { NetId = netId, rotation = rotation }, DeliveryMethod.ReliableOrdered); } - public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudio, bool viaChainInteraction) + public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler, Coupler otherCoupler = null) { - SendPacketToServer(new CommonTrainCouplePacket { - NetId = coupler.train.GetNetId(), - IsFrontCoupler = coupler.isFrontCoupler, - OtherNetId = otherCoupler.train.GetNetId(), - OtherCarIsFrontCoupler = otherCoupler.isFrontCoupler, - PlayAudio = playAudio, - ViaChainInteraction = viaChainInteraction - }, DeliveryMethod.ReliableUnordered); - } + ushort couplerNetId = coupler?.train?.GetNetId() ?? 0; + ushort otherCouplerNetId = otherCoupler?.train?.GetNetId() ?? 0; + bool couplerIsFront = coupler?.isFrontCoupler ?? false; + bool otherCouplerIsFront = otherCoupler?.isFrontCoupler ?? false; - public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) - { - SendPacketToServer(new CommonTrainUncouplePacket { - NetId = coupler.train.GetNetId(), - IsFrontCoupler = coupler.isFrontCoupler, - PlayAudio = playAudio, - ViaChainInteraction = viaChainInteraction, - DueToBrokenCouple = dueToBrokenCouple - }, DeliveryMethod.ReliableUnordered); + 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) + //{ + // 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, + // Value = (byte)coupler.state, + // OtherNetId = otherCouplerNetId, //otherCoupler.train.GetNetId(), + // OtherState = (byte)otherCoupler.state, + // OtherCarIsFrontCoupler = otherCoupler.isFrontCoupler, + // PlayAudio = playAudio, + // ViaChainInteraction = viaChainInteraction + // }, DeliveryMethod.ReliableUnordered); + //} + 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); + }, DeliveryMethod.ReliableOrdered); } 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; + } + + LogDebug(() => $"SendHoseDisconnected({coupler.train.ID}, {coupler.isFrontCoupler}, {playAudio})"); + + SendPacketToServer(new CommonHoseDisconnectedPacket + { + NetId = couplerNetId, IsFront = coupler.isFrontCoupler, PlayAudio = playAudio - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } 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); + }, DeliveryMethod.ReliableOrdered); } public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playAudio) { - SendPacketToServer(new CommonMuDisconnectedPacket { + + SendPacketToServer(new CommonMuDisconnectedPacket + { NetId = netId, IsFront = cable.isFront, PlayAudio = playAudio - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendCockState(ushort netId, Coupler coupler, bool isOpen) { - SendPacketToServer(new CommonCockFiddlePacket { + SendPacketToServer(new CommonCockFiddlePacket + { NetId = netId, IsFront = coupler.isFrontCoupler, IsOpen = isOpen - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendBrakeCylinderReleased(ushort netId) { - SendPacketToServer(new CommonBrakeCylinderReleasePacket { + SendPacketToServer(new CommonBrakeCylinderReleasePacket + { NetId = netId - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } public void SendHandbrakePositionChanged(ushort netId, float position) { - SendPacketToServer(new CommonHandbrakePositionPacket { + SendPacketToServer(new CommonHandbrakePositionPacket + { NetId = netId, Position = position }, DeliveryMethod.ReliableOrdered); } - public void SendPorts(ushort netId, string[] portIds, float[] portValues) + public void SendAddCoal(ushort netId, float coalMassDelta) + { + SendPacketToServer(new ServerboundAddCoalPacket + { + NetId = netId, + CoalMassDelta = coalMassDelta + }, DeliveryMethod.ReliableOrdered); + } + + public void SendTenderCoalPileInteraction(ushort netId, float coalMassDelta) + { + SendPacketToServer(new ServerboundTenderCoalPacket + { + NetId = netId, + CoalMassDelta = coalMassDelta + }, DeliveryMethod.ReliableOrdered); + } + + public void SendFireboxIgnition(ushort netId) { - SendPacketToServer(new CommonTrainPortsPacket { + SendPacketToServer(new ServerboundFireboxIgnitePacket + { + NetId = netId, + }, DeliveryMethod.ReliableOrdered); + } + + public void SendPorts(ushort netId, uint[] portIds, float[] portValues) + { + 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) + public void SendFuses(ushort netId, uint[] fuseIds, bool[] fuseValues) { - SendPacketToServer(new CommonTrainFusesPacket { + SendPacketToServer(new CommonTrainFusesPacket + { NetId = netId, FuseIds = fuseIds, FuseValues = fuseValues - }, DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableOrdered); } 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 +1513,135 @@ 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 SendWarehouseRequest(WarehouseAction action, ushort netId) + { + SendPacketToServer(new ServerboundWarehouseMachineControllerRequestPacket + { + NetId = netId, + WarehouseAction = action, + }, DeliveryMethod.ReliableUnordered); + } + + public void SendChat(string message) + { + SendPacketToServer(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + + } + + public void SendPitStopInteractionPacket(ushort netId, PitStopStationInteractionType interaction, ResourceType? resource, float state) + { + LogDebug(() => $"SendPitStopInteractionPacket({netId}, [{interaction}], {resource}, {state})"); + + int res = resource == null ? 0 : (int)resource; + SendPacketToServer(new CommonPitStopInteractionPacket + { + Tick = NetworkLifecycle.Instance.Tick, + NetId = netId, + InteractionType = interaction, + ResourceType = res, + Value = state + }, DeliveryMethod.ReliableOrdered); + } + + public void SendPitStopPlugInteractionPacket + ( + ushort netId, + PlugInteractionType interaction, + Vector3? position = null, + Quaternion? rotation = null, + ushort trainCarNetId = 0, + sbyte socketIndex = -1 + ) + { + LogDebug(() => $"SendPitStopPlugInteractionPacket({netId}, {interaction}, pos: {position}, rot: {rotation}, trainNetId: {trainCarNetId}, socketIndex: {socketIndex})"); + + SendNetSerializablePacketToServer(new CommonPitStopPlugInteractionPacket + { + NetId = netId, + InteractionType = interaction, + TrainCarNetId = trainCarNetId, + SocketIndex = socketIndex, + Position = position, + Rotation = rotation, + + }, DeliveryMethod.ReliableOrdered); + } + + public void SendItemsChangePacket(List items) + { + Log($"Sending CommonItemChangePacket with {items.Count()} items"); + //SendPacketToServer(new CommonItemChangePacket { Items = items }, + // DeliveryMethod.ReliableUnordered); + + SendNetSerializablePacketToServer(new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableOrdered); + } + + public void SendPaintThemeChange(NetworkedTrainCar netTraincar, TrainCarPaint.Target targetArea, uint themeId) + { + Log($"Sending paint theme change for {netTraincar.CurrentID}"); + + SendPacketToServer(new CommonPaintThemePacket { NetId = netTraincar.NetId, TargetArea = targetArea, PaintThemeId = themeId }, DeliveryMethod.ReliableUnordered); + } + + public void SendCashRegisterAction(ushort netId, CashRegisterAction action, double amount = 0.0f) + { + SendPacketToServer( + new CommonCashRegisterWithModulesActionPacket + { + NetId = netId, + Action = action, + Amount = amount + }, + DeliveryMethod.ReliableOrdered + ); + } + + public void SendTrainControlAuthorityRequest(ushort netId, uint portNetId, bool requestAuthority) + { + SendPacketToServer + ( + new ServerboundTrainControlAuthorityPacket + { + NetId = netId, + PortNetId = portNetId, + RequestAuthority = requestAuthority + }, + DeliveryMethod.ReliableOrdered + ); + } + + public void SendGenericSwitchState(uint netId, bool isOn) + { + SendPacketToServer + ( + new CommonGenericSwitchStatePacket + { + NetId = netId, + IsOn = isOn + }, + deliveryMethod: DeliveryMethod.ReliableOrdered + ); + } #endregion } diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs new file mode 100644 index 00000000..532475f9 --- /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 93b5cd8b..9c215ea2 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -1,73 +1,118 @@ -using System; -using System.Net; -using System.Net.Sockets; using LiteNetLib; using LiteNetLib.Utils; +using Multiplayer.API; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Serialization; +using Multiplayer.Networking.TransportLayers; +using System; +using System.Net; +using System.Net.Sockets; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers; -public abstract class NetworkManager : INetEventListener +public abstract class NetworkManager { + protected const int LATENCY_FLAG = 150; + 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); + 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(TrainCarHealthData.Serialize, TrainCarHealthData.Deserialize); + netPacketProcessor.RegisterNestedType(PitStopPlugMappingData.Serialize, PitStopPlugMappingData.Deserialize); + netPacketProcessor.RegisterNestedType(LocoResourceModuleData.Serialize, LocoResourceModuleData.Deserialize); + netPacketProcessor.RegisterNestedType(PitStopPlugData.Serialize, PitStopPlugData.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); netPacketProcessor.RegisterNestedType(Vector3Serializer.Serialize, Vector3Serializer.Deserialize); + netPacketProcessor.RegisterNestedType(ColorSerializer.Serialize, ColorSerializer.Deserialize); } 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 virtual bool Start() + { + NetIdProvider.Instance.CheckInitialization(); + return transport.Start(); + } + public virtual bool Start(IPAddress ipv4, IPAddress ipv6, int port) + { + 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 void Stop() + public virtual void Stop() { - netManager.Stop(true); + transport.Stop(true); + + transport.OnConnectionRequest -= OnConnectionRequest; + transport.OnPeerConnected -= OnPeerConnected; + transport.OnPeerDisconnected -= OnPeerDisconnected; + transport.OnNetworkReceive -= OnNetworkReceive; + transport.OnNetworkError -= OnNetworkError; + transport.OnNetworkLatencyUpdate -= OnNetworkLatencyUpdate; + Settings.OnSettingsUpdated -= OnSettingsUpdated; + + NetIdProvider.Destroy(NetIdProvider.Instance); } protected NetDataWriter WritePacket(T packet) where T : class, new() @@ -77,17 +122,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 +157,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 +172,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 00000000..6d836f79 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -0,0 +1,312 @@ +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Text; +using System; + +namespace Multiplayer.Networking.Managers.Server; + +public delegate void ChatCommandCallbackInternal(string message, ServerPlayer sender); +public delegate bool ChatFilterDelegateInternal(ref string message, ServerPlayer sender); + +public 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 = "help"; + public const string COMMAND_HELP_SHORT = "?"; + public const string COMMAND_LOG = "log"; + public const string COMMAND_LOG_SHORT = "l"; + public const string COMMAND_KICK = "kick"; + + public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; + public const string MESSAGE_COLOUR_HELP = "00FF00"; + + private readonly Dictionary _registeredCommands = []; + private readonly Dictionary> _registeredHelpMessages = []; + private readonly List _chatFilters = []; + + public ChatManager() + { + RegisterBuiltInCommands(); + } + + private void RegisterBuiltInCommands() + { + RegisterChatCommand + ( + COMMAND_SERVER, + COMMAND_SERVER_SHORT, + () => $"{Locale.CHAT_HELP_SERVER_MSG}" + + $"\r\n\t\t/{COMMAND_SERVER} <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/{COMMAND_SERVER_SHORT} <{Locale.CHAT_HELP_MSG}>", + (message, sender) => ServerMessage(message, sender, null) + ); + + RegisterChatCommand + ( + COMMAND_WHISPER, + COMMAND_WHISPER_SHORT, + () => $"{Locale.CHAT_HELP_WHISPER_MSG}" + + $"\r\n\t\t/{COMMAND_WHISPER} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/{COMMAND_WHISPER_SHORT} <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>", + WhisperMessage + ); + + RegisterChatCommand + ( + COMMAND_HELP, + COMMAND_HELP_SHORT, + () => $"{Locale.CHAT_HELP_HELP}" + + $"\r\n\t\t/{COMMAND_HELP}" + + $"\r\n\t\t/{COMMAND_HELP_SHORT}", + HelpMessage + ); + + RegisterChatCommand + ( + COMMAND_KICK, + null, + () => $"Kick a player from the server (must be host)" + + $"\r\n\t\t/{COMMAND_KICK}", + KickMessage + ); + +#if DEBUG + RegisterChatCommand + ( + COMMAND_LOG, + COMMAND_LOG_SHORT, + null, + (args, sender) => + Multiplayer.specLog = !Multiplayer.specLog); +#endif + } + + public bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallbackInternal callback) + { + if (string.IsNullOrEmpty(commandLong) || callback == null) + return false; + + if (_registeredCommands.ContainsKey(commandLong.ToLower()) || + (!string.IsNullOrEmpty(commandShort) && _registeredCommands.ContainsKey(commandShort.ToLower()))) + return false; + + _registeredCommands[commandLong.ToLower()] = callback; + + if (!string.IsNullOrEmpty(commandShort) && !_registeredCommands.ContainsKey(commandShort.ToLower())) + { + _registeredCommands[commandShort.ToLower()] = callback; + } + + if (helpMessage != null) + { + _registeredHelpMessages[commandLong.ToLower()] = helpMessage; + } + + return true; + } + + public void RegisterChatFilter(ChatFilterDelegateInternal callback) + { + if (callback != null) + { + _chatFilters.Add(callback); + } + } + + public void ProcessMessage(string message, ServerPlayer sender) + { + + if (string.IsNullOrEmpty(message)) + return; + + Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\')"); + + //Check if we have a command + if (message.StartsWith("/")) + { + string[] messageParams = message.Substring(1).Split(' '); + string command = messageParams[0].ToLower(); + + Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\') starts with, substr: {message.Substring(0)}), command: {command}"); + + //check registered commands + if (!string.IsNullOrEmpty(command) && _registeredCommands.TryGetValue(command, out var commandCallback)) + { + //remove the command, leading slash and trailing space + var cleanedMessage = message.Substring(command.Length + 1).Trim(); + + Multiplayer.LogDebug(() => $"ProcessMessage(\'{message}\') cleaned message: {cleanedMessage}"); + + commandCallback(cleanedMessage, sender); + + return; + } + } + + //not a server command, process as normal message + ProcessChatMessage(message, sender); + } + + private void ProcessChatMessage(string message, ServerPlayer sender) + { + if (sender == null) + return; + + //clean up the message to stop format injection + message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); + + //call each filter until either a filter returns false or all filters have been called + foreach (var filter in _chatFilters) + { + if (!filter(ref message, sender)) + return; + } + + message = $"{sender.Username}: {message}"; + NetworkLifecycle.Instance.Server.SendChat(message, sender); + } + + public void ServerMessage(string message, ServerPlayer sender, ServerPlayer exclude = null) + { + //If user is not the host, we should ignore - will require changes for dedicated server + if (sender != null && !NetworkLifecycle.Instance.IsHost(sender)) + return; + + message = $"{message}"; + NetworkLifecycle.Instance.Server.SendChat(message, exclude); + } + + private void WhisperMessage(string message, ServerPlayer sender) + { + Multiplayer.LogDebug(() => $"Whispering: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}"); + + if (sender == null) + return; + + if (string.IsNullOrEmpty(message)) + return; + + string[] parts = message.Split([' '], 2, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 2) + return; + + string recipientName = parts[0]; + string whisperMessage = parts[1]; + + + Multiplayer.LogDebug(() => $"Whispering parse 1: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}, peerName: {recipientName}"); + + //look up the peer ID + var recipient = ServerPlayerFromUsername(recipientName); + if (recipient == null) + { + Multiplayer.LogDebug(() => $"Whispering failed: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}, peerName: {recipientName}"); + + whisperMessage = $"{Locale.Get(Locale.CHAT_WHISPER_NOT_FOUND_KEY, recipientName)}"; + NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, sender); + return; + } + + Multiplayer.LogDebug(() => $"Whispering parse 2: \"{message}\", sender: {sender?.Username}, senderID: {sender?.PlayerId}, peerName: {recipientName}, peerID: {recipient?.PlayerId}"); + + //clean up the message to stop format injection + whisperMessage = Regex.Replace(whisperMessage, "", string.Empty, RegexOptions.IgnoreCase); + + //call each chat filter until either a filter returns false or all filters have been called + foreach (var filter in _chatFilters) + { + if (!filter(ref message, sender)) + return; + } + + whisperMessage = "" + sender.Username + ": " + whisperMessage + ""; + + NetworkLifecycle.Instance.Server.SendWhisper(whisperMessage, recipient); + } + + public void KickMessage(string message, ServerPlayer sender) + { + ServerPlayer playerToKick; + string playerName; + string whisper; + + //If user is not the host, we should ignore - will require changes for dedicated server + if (sender == null || !NetworkLifecycle.Instance.IsHost(sender)) + return; + + playerName = message.Split(' ')[0]; + if (string.IsNullOrEmpty(playerName)) + return; + + playerToKick = ServerPlayerFromUsername(playerName); + + if (playerToKick == null || NetworkLifecycle.Instance.IsHost(playerToKick)) + { + whisper = $"{Locale.Get(Locale.CHAT_KICK_UNABLE_KEY, [playerName])}"; + } + else + { + whisper = $"{Locale.Get(Locale.CHAT_KICK_KICKED_KEY, [playerName])}"; + + NetworkLifecycle.Instance.Server.KickPlayer(playerToKick); + } + + NetworkLifecycle.Instance.Server.SendWhisper(whisper, sender); + } + + private void HelpMessage(string _, ServerPlayer player) + { + + if (player == null) + return; + + Multiplayer.LogDebug(() => $"HelpMessage()"); + + StringBuilder sb = new($"{Locale.CHAT_HELP_AVAILABLE}"); + + foreach (var helpMessage in _registeredHelpMessages) + { + var help = helpMessage.Value?.Invoke(); + if (help != null) + sb.AppendLine("\r\n\t" + help); + } + + sb.AppendLine(""); + + /* + * $"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 playerToKick" + + "\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(sb.ToString(), player); + } + + + private ServerPlayer ServerPlayerFromUsername(string playerName) + { + + if (string.IsNullOrEmpty(playerName)) + return null; + + return NetworkLifecycle.Instance.Server.ServerPlayers.Where(p => p.Username == playerName).FirstOrDefault(); + } +} diff --git a/Multiplayer/Networking/Managers/Server/CullingManager.cs b/Multiplayer/Networking/Managers/Server/CullingManager.cs new file mode 100644 index 00000000..e2d6edf8 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/CullingManager.cs @@ -0,0 +1,120 @@ +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace Multiplayer.Networking.Managers.Server; + +public class CullingManager : IDisposable +{ + private const float DEFAULT_CULL_SQR_DISTANCE = 250000f; + + public event Action PlayerEnteredActivationRegion; + public event Action PlayerEnteredCullingRegion; + + public List ActivePlayers => playerToLastNearbyTime.Keys.ToList(); + + private readonly Dictionary playerToLastNearbyTime = []; + private readonly float _checkInterval = 2f; + private readonly float _cullSqrDistance = DEFAULT_CULL_SQR_DISTANCE; + private readonly float _activationSqrDistance = DEFAULT_CULL_SQR_DISTANCE / 2; + private readonly float _cullDelay = 3f; + private GameObject _referenceObject = null; + + private Coroutine checkCoro; + + public CullingManager(float checkInterval, float cullSqrDistance, float activationSqrDistance, float cullDelay, GameObject referenceObject) + { + if (checkInterval > 0) + _checkInterval = checkInterval; + + if (cullSqrDistance > 0) + _cullSqrDistance = cullSqrDistance; + + if (activationSqrDistance > 0) + _activationSqrDistance = activationSqrDistance; + + if (cullDelay >= 0) + _cullDelay = cullDelay; + + if (referenceObject != null) + _referenceObject = referenceObject; + else + throw new Exception("Reference object is null!"); + + checkCoro = CoroutineManager.Instance.StartCoroutine(PlayerDistanceChecker()); + + NetworkLifecycle.Instance.Server.PlayerDisconnected += OnPlayerDisconnected; + } + + public void Dispose() + { + if (checkCoro != null) + CoroutineManager.Instance.Stop(checkCoro); + + NetworkLifecycle.Instance.Server.PlayerDisconnected -= OnPlayerDisconnected; + } + + //todo: fix when merged with ModAPI branch + private void OnPlayerDisconnected(ServerPlayer serverPlayer) + { + var player = playerToLastNearbyTime.Keys.Where(p => p == serverPlayer).FirstOrDefault(); + + if (player == null) + return; + + playerToLastNearbyTime.Remove(player); + } + + private IEnumerator PlayerDistanceChecker() + { + //wait for game to finish loading + yield return new WaitForSeconds(2f); + + while (true) + { + yield return new WaitForSeconds(_checkInterval); + + //if not active then there is no one close by + if (_referenceObject != null && _referenceObject.activeInHierarchy) + { + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (player.PlayerId == NetworkLifecycle.Instance.Server.SelfId || !player.IsLoaded) + continue; + + float sqrDistance = (player.WorldPosition - _referenceObject.transform.position).sqrMagnitude; + + bool initialised = playerToLastNearbyTime.TryGetValue(player, out float lastVisit); + + if (initialised && sqrDistance > _cullSqrDistance) + { + // Too far away for too long, stop tracking + if ((Time.time - lastVisit) > _cullDelay) + { + playerToLastNearbyTime.Remove(player); + PlayerEnteredCullingRegion?.Invoke(player); + } + + continue; + } + + if (!initialised) + { + //make sure they are close by before we add them to the nearby list + if (sqrDistance > _activationSqrDistance) + continue; + + PlayerEnteredActivationRegion?.Invoke(player); + } + + //player nearby recently, update time + playerToLastNearbyTime[player] = Time.time; + } + } + } + } +} diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs new file mode 100644 index 00000000..16ac1bc2 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -0,0 +1,536 @@ +using DV.Platform.Steam; +using DV.WeatherSystem; +using LiteNetLib.Utils; +using LiteNetLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Packets.Unconnected; +using Multiplayer.Utils; +using Newtonsoft.Json; +using Steamworks.Data; +using Steamworks; +using System.Collections; +using System.Linq; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Net; +using System.Text.RegularExpressions; +using System; +using UnityEngine.Networking; +using UnityEngine; + +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", "Visibility", "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() + { + bool destroy = false; + + server = NetworkLifecycle.Instance.Server; + + if (server == null) + { + server.LogError($"LobbyServerManager Server is null"); + destroy = true; + } + else if (server?.ServerData == null) + { + Multiplayer.LogError($"LobbyServerManager Server Data is null"); + destroy = true; + } + + if (destroy) + { + Multiplayer.LogError($"Failed to load LobbyServerManager"); + Destroy(this); + } + } + + public IEnumerator Start() + { + if (server == null || server.ServerData == null) + yield break; + + //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(); + + if (!string.IsNullOrEmpty(Multiplayer.Settings.Ipv4AddressCheck)) + { + StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); + } + else + { + server.LogWarning("Ipv4AddressCheck URL is null or empty, skipping IPv4 detection"); + initialised = true; + } + + while (!initialised) + yield return null; + + server.Log + ( + $"\r\nPublic IPv4: {server.ServerData.ipv4}\r\n" + + $"Public IPv6: {server.ServerData.ipv6}\r\n" + + $"Private IPv4: {server.ServerData.LocalIPv4}" + ); + + if (server.ServerData.Visibility >= ServerVisibility.Private) + { + 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?.Visibility == ServerVisibility.Private || !sendUpdates) + { + server.ServerData.CurrentPlayers = server.PlayerCount; + } + + //Keep LAN discovery running + discoveryManager?.PollEvents(); + } + + #endregion + + #region Steam Lobby + public async void CreateSteamLobby() + { + if (server == null || server.ServerData == null) + return; + + // 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, SteamworksUtils.LOBBY_MP_MOD_KEY); //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); + + //Set correct visibility + if (server.ServerData.Visibility == ServerVisibility.Private) + lobby?.SetPrivate(); + else if (server.ServerData.Visibility == ServerVisibility.Friends) + lobby?.SetFriendsOnly(); + else if (server.ServerData.Visibility == ServerVisibility.Public) + lobby?.SetPublic(); + + 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 f5129b2a..9c0468fa 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using DV; +using DV.Customization.Paint; using DV.InventorySystem; using DV.Logic.Job; using DV.Scenarios.Common; @@ -11,268 +9,946 @@ using Humanizer; using LiteNetLib; using LiteNetLib.Utils; +using MPAPI.Interfaces.Packets; +using MPAPI.Types; +using Multiplayer.API; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; 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.Packets.Serverbound.Jobs; +using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.Packets.Unconnected; +using Multiplayer.Networking.TransportLayers; +using Multiplayer.Patches.MainMenu; using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; using UnityEngine; -using UnityModManagerNet; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { + private const int WEATHER_UPDATE_INTERVAL = 30; //seconds + + public Action PlayerConnected; + public Action PlayerDisconnected; + public Action PlayerReady; 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(); //Queue for players attempting to join while server is loading + + private readonly Dictionary serverPlayers = []; //player Id to ServerPlayer mapping + private readonly Dictionary peers = []; //player Id to peer mapping + private readonly Dictionary peerToPlayer = []; //peer to ServerPlayer mapping + + private LobbyServerManager lobbyServerManager; + public readonly bool IsSinglePlayer; + public LobbyServerData ServerData; + public RerailController rerailController; public IReadOnlyCollection ServerPlayers => serverPlayers.Values; - public int PlayerCount => netManager.ConnectedPeersCount; + public int PlayerCount => ServerPlayers.Count; + + private ITransportPeer _selfPeer; + public ITransportPeer SelfPeer + { + get + { + if (_selfPeer != null) + return _selfPeer; - private static NetPeer selfPeer => NetworkLifecycle.Instance.Client?.selfPeer; - public static byte SelfId => (byte)selfPeer.Id; - private readonly ModInfo[] serverMods; + peers.TryGetValue(SelfId, out _selfPeer); + return _selfPeer; + } + } + + public byte SelfId => NetworkLifecycle.Instance.Client?.PlayerId ?? 0; public readonly IDifficulty Difficulty; private bool IsLoaded; - public NetworkServer(IDifficulty difficulty, Settings settings) : base(settings) + private readonly ChatManager _chatManager = new(); + public ChatManager ChatManager => _chatManager; + + private uint lastTick; + + public NetworkServer(IDifficulty difficulty, Settings settings, bool singlePlayer, LobbyServerData serverData) : base(settings) { + Log($"Server created for {(singlePlayer ? "single player" : "multiplayer")} game"); + + IsSinglePlayer = singlePlayer; + ServerData = serverData; Difficulty = difficulty; - serverMods = ModInfo.FromModEntries(UnityModManager.modEntries); } - 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); + + 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() + { + WorldStreamingInit.LoadingFinished -= OnLoaded; + + if (lobbyServerManager != null) + { + lobbyServerManager.RemoveFromLobbyServer(); + UnityEngine.Object.Destroy(lobbyServerManager); + } + + //Alert all clients (except host) + var packet = WritePacket(new ClientboundDisconnectPacket()); + foreach (var peer in peers.Values) + { + if (peer != SelfPeer) + peer?.Disconnect(packet); + } + + //Reset player ID pool + foreach (var player in serverPlayers.Values) + player.Dispose(); + + NetworkLifecycle.Instance.OnTick -= OnTick; + + 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(OnCommonTrainUncouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); + netPacketProcessor.SubscribeReusable(OnServerboundTrainControlAuthorityPacket); + netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); + netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); + netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTenderCoalPacket); + netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundWarehouseMachineControllerRequestPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); + + netPacketProcessor.SubscribeReusable(OnCommonPitStopInteractionPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonPitStopPlugInteractionPacket); + + netPacketProcessor.SubscribeReusable(OnCommonCashRegisterWithModulesActionPacket); + + netPacketProcessor.SubscribeReusable(OnCommonGenericSwitchStatePacket); + } + + //allow mods to register their own packets + public void RegisterExternalPacket(ServerPacketHandler handler) where T : class, IPacket, new() + { + netPacketProcessor.SubscribeReusable((packet, peer) => + { + var serverPlayer = TryGetServerPlayer(peer, out var player) ? new ServerPlayerWrapper(player) : null; + handler(packet, serverPlayer); + }); + } + + public void RegisterExternalSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new() + { + netPacketProcessor.SubscribeNetSerializable, ITransportPeer>((wrapper, peer) => + { + var serverPlayer = TryGetServerPlayer(peer, out var player) ? new ServerPlayerWrapper(player) : null; + handler(wrapper.Packet, serverPlayer); + }, + () => new ExternalSerializablePacketWrapper() + ); } private void OnLoaded() { + if (!IsSinglePlayer) + { + lobbyServerManager = NetworkLifecycle.Instance.GetOrAddComponent(); + } + Log($"Server loaded, processing {joinQueue.Count} queued players"); IsLoaded = true; + + //We should initialise object here for dedicated servers, rather than relying on the existance of a client + NetworkedPitStopStation.InitialisePitStops(); + NetworkedCashRegisterWithModules.InitialiseCashRegisters(); + 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."); + } + } + + lastTick = NetworkLifecycle.Instance.Tick; + NetworkLifecycle.Instance.OnTick += OnTick; + } + + private void OnTick(uint tick) + { + if (!IsLoaded) + return; + + if ((NetworkLifecycle.Instance.Tick - lastTick) > NetworkLifecycle.TICK_RATE * WEATHER_UPDATE_INTERVAL) + { + SendWeatherState(); + lastTick = NetworkLifecycle.Instance.Tick; } } - public bool TryGetServerPlayer(NetPeer peer, out ServerPlayer player) + public bool TryGetServerPlayer(ITransportPeer peer, out ServerPlayer player) { - return serverPlayers.TryGetValue((byte)peer.Id, out player); + return peerToPlayer.TryGetValue(peer, out player); } - public bool TryGetNetPeer(byte id, out NetPeer peer) + public bool TryGetServerPlayer(byte playerId, out ServerPlayer player) { - return netPeers.TryGetValue(id, out peer); + return serverPlayers.TryGetValue(playerId, out player); } #region Net Events - public override void OnPeerConnected(NetPeer peer) - { } + public override void OnPeerConnected(ITransportPeer peer) + { + LogDebug(() => $"OnPeerConnected({peer.Id})"); + } - 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}"); + LogDebug(() => $"OnPeerDisconnected({peer.Id})"); + if (!peerToPlayer.TryGetValue(peer, out ServerPlayer player)) + LogWarning($"Peer {peer.GetType()}, peerId: {peer.Id} disconnected but no player found"); + else + Log($"Player {player?.Username} disconnected: {disconnectReason}"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); - serverPlayers.Remove(id); - netPeers.Remove(id); - netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket { - Id = id - }), DeliveryMethod.ReliableUnordered); + serverPlayers.Remove(player.PlayerId); + peers.Remove(player.PlayerId); + peerToPlayer.Remove(peer); + + SendPacketToAll + ( + new ClientboundPlayerDisconnectPacket + { + PlayerId = player.PlayerId + }, + DeliveryMethod.ReliableUnordered + ); + + PlayerDisconnected?.Invoke(player); + + player?.Dispose(); } - public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) + public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { - ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() { - Id = (byte)peer.Id, + if (!TryGetServerPlayer(peer, out var player)) + return; + + ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() + { + PlayerId = player.PlayerId, Ping = latency }; SendPacketToAll(clientboundPingUpdatePacket, DeliveryMethod.ReliableUnordered, peer); - SendPacket(peer, new ClientboundTickSyncPacket { + if (latency > LATENCY_FLAG) + { + LogWarning($"High Ping Detected! Player: \"{player.Username}\", ping: {latency}ms"); + } + + // Ensure we don't send a TickSync packet to ourselves + if (peer == SelfPeer) + return; + + 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 #region Packet Senders - private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() + private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, bool excludeSelf = false) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in netPeers) - kvp.Value.Send(writer, deliveryMethod); + foreach (var peer in peers.Values) + { + if (excludeSelf && peer == SelfPeer) + continue; + + peer?.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, bool excludeSelf = false) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in netPeers) + foreach (var peer in peers.Values) { - if (kvp.Key == excludePeer.Id) + if (peer == excludePeer || (excludeSelf && peer == SelfPeer)) continue; - kvp.Value.Send(writer, deliveryMethod); + + peer?.Send(writer, deliveryMethod); } } + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, bool excludeSelf = false) where T : INetSerializable, new() + { + NetDataWriter writer = WriteNetSerializablePacket(packet); + foreach (var peer in peers.Values) + { + if (excludeSelf && peer == SelfPeer) + continue; + + peer?.Send(writer, deliveryMethod); + } + } + + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer, bool excludeSelf = false) where T : INetSerializable, new() + { + NetDataWriter writer = WriteNetSerializablePacket(packet); + foreach (var peer in peers.Values) + { + if (peer == excludePeer || (excludeSelf && peer == SelfPeer)) + continue; + peer?.Send(writer, deliveryMethod); + } + } + + #region Mod Packets + public void SendExternalPacketToAll(T packet, bool reliable, bool excludeSelf = false) where T : class, IPacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacketToAll(packet, deliveryMethod, excludeSelf); + } + + public void SendExternalPacketToAll(T packet, bool reliable, ITransportPeer excludePeer, bool excludeSelf = false) where T : class, IPacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + + if (excludePeer == null) + SendPacketToAll(packet, deliveryMethod, excludeSelf); + else + SendPacketToAll(packet, deliveryMethod, excludePeer, excludeSelf); + } + + public void SendExternalSerializablePacketToAll(T packet, bool reliable, bool excludeSelf = false) where T : class, ISerializablePacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludeSelf); + } + + public void SendExternalSerializablePacketToAll(T packet, bool reliable, ITransportPeer excludePeer, bool excludeSelf = false) where T : class, ISerializablePacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + + if (excludePeer == null) + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludeSelf); + else + SendNetSerializablePacketToAll(wrapper, deliveryMethod, excludePeer, excludeSelf); + } + + public void SendExternalPacketToPlayer(T packet, ITransportPeer peer, bool reliable) where T : class, IPacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + SendPacket(peer, packet, deliveryMethod); + } + + public void SendExternalSerializablePacketToPlayer(T packet, ITransportPeer peer, bool reliable) where T : class, ISerializablePacket, new() + { + var deliveryMethod = reliable ? DeliveryMethod.ReliableUnordered : DeliveryMethod.Unreliable; + var wrapper = new ExternalSerializablePacketWrapper { Packet = packet }; + + SendNetSerializablePacket(peer, wrapper, deliveryMethod); + } + + #endregion + + public void KickPlayer(ServerPlayer player) + { + if (player == null || player.Peer == null) + return; + + player.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, excludeSelf: true); + } + + public void SendWeatherState(ITransportPeer peer = null) + { + var packet = WeatherDriver.Instance.GetSaveData(Globals.G.GameParams.WeatherEditorAlwaysAllowed).ToObject(); + + if (peer != null) + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + else + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludeSelf: true); + } + + 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, excludeSelf: true); } - public void SendDestroyTrainCar(TrainCar trainCar) + public void SendDestroyTrainCar(NetworkedTrainCar netTrainCar, ITransportPeer peer = null) { - SendPacketToAll(new ClientboundDestroyTrainCarPacket { - NetId = trainCar.GetNetId() - }, DeliveryMethod.ReliableOrdered, selfPeer); + //ushort netID = trainCar.GetNetId(); + Log($"Sending DestroyTrainCarPacket for [{netTrainCar.CurrentID} {netTrainCar.NetId}]"); + + if (netTrainCar.NetId == 0) + { + LogWarning($"SendDestroyTrainCar failed. [{netTrainCar.CurrentID} {netTrainCar.NetId}]"); + return; + } + + var packet = new ClientboundDestroyTrainCarPacket { NetId = netTrainCar.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); + //LogDebug(() => $"Sending Physics packet for netId: {packet.FirstNetId}, tick: {packet.Tick}"); + SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, SelfPeer); } - public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte cargoModelIndex) + public void SendBrakeState(ushort netId, float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure, float overheatPercent, float overheatReductionFactor, float temperature) { - Car logicCar = trainCar.logicCar; - CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; - SendPacketToAll(new ClientboundCargoStatePacket { + SendPacketToAll(new ClientboundBrakeStateUpdatePacket + { NetId = netId, + MainReservoirPressure = mainReservoirPressure, + BrakePipePressure = brakePipePressure, + BrakeCylinderPressure = brakeCylinderPressure, + OverheatPercent = overheatPercent, + OverheatReductionFactor = overheatReductionFactor, + Temperature = temperature + }, DeliveryMethod.ReliableOrdered, SelfPeer); + + //LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); + } + + public void SendCargoState(NetworkedTrainCar netTraincar, bool isLoading, byte cargoModelIndex) + { + Car logicCar = netTraincar?.TrainCar?.logicCar; + + //LogDebug(() => $"SendCargoState({netTraincar?.CurrentID}, isLoading: {isLoading}, cargoModelIndex: {cargoModelIndex}), logicCar: {logicCar?.ID}, WareHouseMachineID: {logicCar.CargoOriginWarehouse?.ID}, warehouse track: {logicCar.CargoOriginWarehouse?.WarehouseTrack?.ID}"); + + Log($"Sending Cargo State for {netTraincar?.CurrentID}, isLoading: {isLoading}, cargoModelIndex: {cargoModelIndex}"); + + if (logicCar == null) + { + LogWarning($"Attempted to send cargo state for {netTraincar?.CurrentID}, but logic car does not exist!"); + return; + } + + CargoType cargoTypeV1 = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; + + CargoTypeLookup.Instance.TryGetNetId(cargoTypeV1, out uint cargoType); + + ushort netMachineId = 0; + if (logicCar.CargoOriginWarehouse != null) + { + if (!WarehouseMachineLookup.TryGetNetId(logicCar.CargoOriginWarehouse, out netMachineId)) + { + Log($"Attempting to send cargo state for {netTraincar.CurrentID}, for warehouse machine at track {logicCar.CargoOriginWarehouse?.WarehouseTrack?.ID}, but Warehouse Machine was not found"); + return; + } + } + + SendPacketToAll(new ClientboundCargoStatePacket + { + NetId = netTraincar.NetId, IsLoading = isLoading, - CargoType = (ushort)cargoType, + CargoTypeNetId = cargoType, CargoAmount = logicCar.LoadedCargoAmount, + CargoHealth = netTraincar.TrainCar.CargoDamage.HealthPercentage, CargoModelIndex = cargoModelIndex, - WarehouseMachineId = logicCar.CargoOriginWarehouse?.ID - }, DeliveryMethod.ReliableOrdered, selfPeer); + WarehouseMachineNetId = netMachineId, + }, DeliveryMethod.ReliableOrdered, SelfPeer); + } + + public void SendPaintThemeChange(NetworkedTrainCar netTraincar, TrainCarPaint.Target targetArea, uint themeNetId, ServerPlayer sendToPlayer = null) + { + var packet = new CommonPaintThemePacket + { + NetId = netTraincar.NetId, + TargetArea = targetArea, + PaintThemeId = themeNetId + }; + + Log($"Sending paint theme change for {netTraincar.CurrentID}"); + + if (sendToPlayer != null) + SendPacket(sendToPlayer.Peer, packet, DeliveryMethod.ReliableUnordered); + else + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, true); + } + + public void SendWarehouseControllerUpdate(ushort netId, bool isLoading, ushort jobNetId, ushort carNetId, uint cargoTypeNetId, WarehouseMachineController.TextPreset preset) + { + LogDebug(() => $"SendWarehouseControllerUpdate({netId}, {isLoading}, {jobNetId}, {carNetId}, {cargoTypeNetId}, {preset})"); + + SendPacketToAll(new ClientboundWarehouseControllerUpdatePacket() + { + NetId = netId, + IsLoading = isLoading, + JobNetId = jobNetId, + CarNetId = carNetId, + CargoTypeNetId = cargoTypeNetId, + Preset = (ushort)preset, + }, + DeliveryMethod.Sequenced, SelfPeer); + } + + public void SendCargoHealthUpdate(ushort netId, float currentHealth) + { + SendPacketToAll(new ClientboundCargoHealthUpdatePacket + { + NetId = netId, + CargoHealth = currentHealth, + }, DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendCarHealthUpdate(ushort netId, float health) + public void SendCarHealthUpdate(ushort netId, TrainCarHealthData health) { - SendPacketToAll(new ClientboundCarHealthUpdatePacket { + + //LogDebug(() => $"Sending Car Health Update for netId {netId}: BodyHP: {health.BodyHP}, WheelsHP: {health.WheelsHP}, MechanicalPT: {health.MechanicalPT}, ElectricalPT: {health.ElectricalPT}, WindowsBroken: {health.WindowsBroken}"); + + 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 { - NetId = netId, - ForceDirection = forceDirection - }, DeliveryMethod.ReliableUnordered, selfPeer); + SendPacketToAll + ( + new ClientboundWindowsBrokenPacket + { + NetId = netId, + ForceDirection = forceDirection + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendWindowsRepaired(ushort netId) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { - NetId = netId - }, DeliveryMethod.ReliableUnordered, selfPeer); + SendPacketToAll + ( + new ClientboundWindowsRepairedPacket + { + NetId = netId + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendMoney(float amount) { - SendPacketToAll(new ClientboundMoneyPacket { - Amount = amount - }, DeliveryMethod.ReliableUnordered, selfPeer); + SendPacketToAll + ( + new ClientboundMoneyPacket + { + Amount = amount + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendLicense(string id, bool isJobLicense) { - SendPacketToAll(new ClientboundLicenseAcquiredPacket { - Id = id, - IsJobLicense = isJobLicense - }, DeliveryMethod.ReliableUnordered, selfPeer); + SendPacketToAll + ( + new ClientboundLicenseAcquiredPacket + { + Id = id, + IsJobLicense = isJobLicense + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendGarage(string id) { - SendPacketToAll(new ClientboundGarageUnlockPacket { - Id = id - }, DeliveryMethod.ReliableUnordered, selfPeer); + SendPacketToAll + ( + new ClientboundGarageUnlockPacket + { + Id = id + }, + DeliveryMethod.ReliableUnordered, + excludeSelf: true + ); } public void SendDebtStatus(bool hasDebt) { - SendPacketToAll(new ClientboundDebtStatusPacket { + SendPacketToAll(new ClientboundDebtStatusPacket + { HasDebt = hasDebt - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); + } + + public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) + { + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + LogDebug(() => $"SendTrainUncouple({coupler.train.ID}, {coupler.isFrontCoupler}, {dueToBrokenCouple}, {viaChainInteraction})"); + + SendPacketToAll( + new CommonTrainUncouplePacket + { + NetId = couplerNetId, + IsFrontCoupler = coupler.isFrontCoupler, + PlayAudio = playAudio, + ViaChainInteraction = viaChainInteraction, + DueToBrokenCouple = dueToBrokenCouple, + }, + DeliveryMethod.ReliableOrdered, + excludeSelf: true + ); + } + + public void SendHoseDisconnected(Coupler coupler, bool playAudio) + { + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + LogWarning($"SendHoseDisconnected failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + LogDebug(() => $"SendHoseDisconnected({coupler.train.ID}, {coupler.isFrontCoupler}, {playAudio})"); + + SendPacketToAll + ( + new CommonHoseDisconnectedPacket + { + NetId = couplerNetId, + IsFront = coupler.isFrontCoupler, + PlayAudio = playAudio + }, + DeliveryMethod.ReliableOrdered, + excludeSelf: true + ); + } + + public void SendCockState(ushort netId, Coupler coupler, bool isOpen) + { + SendPacketToAll + ( + new CommonCockFiddlePacket + { + NetId = netId, + IsFront = coupler.isFrontCoupler, + IsOpen = isOpen + }, + DeliveryMethod.ReliableOrdered, + true + ); + } + + public void SendTrainControlAuthorityUpdate(ushort netId, uint portNetId, ControlAuthorityState state, ServerPlayer sendToPlayer = null, ServerPlayer excludePlayer = null) + { + var packet = new ClientboundTrainControlAuthorityUpdatePacket + { + NetId = netId, + PortNetId = portNetId, + State = state + }; + + if (sendToPlayer == null) + if (excludePlayer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludeSelf: true); + else + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludePlayer.Peer, true); + else + SendPacket(sendToPlayer.Peer, packet, DeliveryMethod.ReliableOrdered); + } + + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) + { + Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); + + var packet = ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs); + + if (peer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, excludeSelf: true); + 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, excludeSelf: true); + } + + public void SendTaskUpdate(ushort taskNetId, TaskState newState, float taskStartTime, float taskFinishTime) + { + Multiplayer.Log($"Sending TaskUpdate for taskNetId {taskNetId}, newState {newState}"); + SendPacketToAll + ( + new ClientboundTaskUpdatePacket + { + TaskNetId = taskNetId, + NewState = newState, + TaskStartTime = taskStartTime, + TaskFinishTime = taskFinishTime + }, + DeliveryMethod.ReliableOrdered, + excludeSelf: true + ); + } + + public void SendItemsChangePacket(List items, ServerPlayer player) + { + Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); + + if (player.Peer != null && player.Peer != SelfPeer) + { + SendNetSerializablePacket(player.Peer, new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableOrdered); + } + } + + public void SendPitStopBulkDataPacket(ushort netId, int carCount, int carIndex, int faucetNotch, LocoResourceModuleData[] stationData, PitStopPlugData[] plugData, ServerPlayer player) + { + LogDebug(() => $"SendPitStopBulkDataPacket({netId}, {carCount}, {carIndex}, {faucetNotch}, {stationData.Count()}, {plugData.Count()}, {player})"); + + var packet = new ClientboundPitStopBulkUpdatePacket + { + NetId = netId, + CarCount = carCount, + CarSelection = carIndex, + FaucetNotch = faucetNotch, + ResourceData = stationData, + PlugData = plugData, + }; + + if (player.Peer != SelfPeer) + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + } + + public void SendPitStopInteractionPacket(ServerPlayer player, CommonPitStopInteractionPacket packet) + { + LogDebug(() => $"SendPitStopInteractionPacket({player.Username}, {packet.NetId})"); + + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + } + + public void SendPitStopPlugInteractionPacket(ServerPlayer player, CommonPitStopPlugInteractionPacket packet) + { + LogDebug(() => $"SendPitStopPlugInteractionPacket({packet.NetId}, {packet.InteractionType}, {packet.PlayerId}, {packet.Position}, {packet.Rotation}, {packet.TrainCarNetId}, {packet.SocketIndex}, {packet.YankForce}, {packet.YankMode})"); + SendNetSerializablePacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + } + + public void SendCashRegisterAction(CommonCashRegisterWithModulesActionPacket packet, ServerPlayer[] players = null) + { + if (players == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, true); + else + foreach (var player in players) + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + } + + public void SendGenericSwitchState(uint netId, bool isOn, ServerPlayer player = null) + { + var packet = new CommonGenericSwitchStatePacket + { + NetId = netId, + IsOn = isOn + }; + + if (player != null) + SendPacket(player.Peer, packet, DeliveryMethod.ReliableOrdered); + else + SendPacketToAll(packet, deliveryMethod: DeliveryMethod.ReliableOrdered, true); + } + + public void SendChat(string message, ServerPlayer exclude = null) + { + var packet = new CommonChatPacket + { + message = message + }; + + if (exclude != null) + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, exclude.Peer); + else + SendPacketToAll(packet, DeliveryMethod.ReliableUnordered); + } + + public void SendWhisper(string message, ServerPlayer recipient) + { + if (!string.IsNullOrEmpty(message) && recipient != null && recipient.Peer != null) + { + NetworkLifecycle.Instance.Server.SendPacket + ( + recipient.Peer, + 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); + LogDebug(() => $"OnServerboundClientLoginPacket from {packet.Username}"); + + // 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,82 +963,134 @@ 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)); return; } - if (packet.BuildMajorVersion != BuildInfo.BUILD_VERSION_MAJOR) + if (packet.BuildVersion != MainMenuControllerPatch.MenuProvider.BuildVersionString) { - LogWarning($"Denied login to incorrect game version! Got: {packet.BuildMajorVersion}, expected: {BuildInfo.BUILD_VERSION_MAJOR}"); - ClientboundServerDenyPacket denyPacket = new() { + LogWarning($"Denied login to incorrect game version! Got: {packet.BuildVersion}, expected: {MainMenuControllerPatch.MenuProvider.BuildVersionString}"); + ClientboundLoginResponsePacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, - ReasonArgs = new[] { BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString() } + ReasonArgs = [MainMenuControllerPatch.MenuProvider.BuildVersionString, packet.BuildVersion.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; - if (!serverMods.SequenceEqual(clientMods)) + var validation = ModCompatibilityManager.Instance.ValidateClientMods(packet.Mods); + if (!validation.IsValid) { - 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() { + + LogWarning($"Denied login due to mod mismatch! {validation.Missing.Count} missing, {validation.Extra} extra"); + LogDebug(() => + { + StringBuilder sb = new("Mod mis-match:"); + sb.AppendLine("Server Mods:"); + foreach (ModInfo mod in ModCompatibilityManager.Instance.GetLocalMods()) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status: {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + sb.AppendLine("\r\nClient Mods:"); + foreach (ModInfo mod in packet.Mods) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status (if known): {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + sb.AppendLine("\r\nMissing Mods:"); + foreach (ModInfo mod in validation.Missing) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status: {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + sb.AppendLine("\r\nExtra Mods:"); + foreach (ModInfo mod in validation.Extra) + sb.AppendLine($"\t{mod.Id} {mod.Version}, Status (if known): {ModCompatibilityManager.Instance.GetCompatibility(mod)}"); + + return sb.ToString(); + }); + + ClientboundLoginResponsePacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, - Missing = missing, - Extra = extra + Missing = validation.Missing.ToArray(), + Extra = validation.Extra.ToArray(), }; request.Reject(WritePacket(denyPacket)); return; } - NetPeer peer = request.Accept(); + ITransportPeer peer = request.Accept(); + + ServerPlayer serverPlayer = new + ( + peer, + overrideUsername, + packet.Username, + guid + ); + + serverPlayers.Add(serverPlayer.PlayerId, serverPlayer); + peerToPlayer.Add(peer, serverPlayer); - ServerPlayer serverPlayer = new() { - Id = (byte)peer.Id, - Username = packet.Username, - Guid = guid + ClientboundLoginResponsePacket acceptPacket = new() + { + Accepted = true, + PlayerId = serverPlayer.PlayerId, }; - serverPlayers.Add(serverPlayer.Id, serverPlayer); + 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)) + LogDebug(() => $"OnServerboundSaveGameDataRequestPacket from peerId: {peer.Id}"); + + if (!TryGetServerPlayer(peer, out ServerPlayer player)) { - LogWarning("Denied save game data request from already connected peer!"); + LogError($"Save game data request received for {peer.GetType()}, peerId: {peer.Id}, but ServerPlayer not found"); + peer.Disconnect(); return; } - TryGetServerPlayer(peer, out ServerPlayer player); + PlayerConnected?.Invoke(player); + + //if (peers.ContainsKey((byte)peer.Id)) + //{ + // LogWarning("Denied save game data request from already connected peer!"); + // return; + //} SendPacket(peer, ClientboundGameParamsPacket.FromGameParams(Globals.G.GameParams), DeliveryMethod.ReliableOrdered); 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; + LogDebug(() => $"OnServerboundClientReadyPacket from peerId: {peer.Id}"); + + if (!peerToPlayer.TryGetValue(peer, out ServerPlayer serverPlayer)) + { + LogError($"Ready packet received for {peer.GetType()}, peerId: {peer.Id}, but ServerPlayer not found"); + peer.Disconnect(); + return; + } // Allow clients to connect before the server is fully loaded if (!IsLoaded) @@ -377,190 +1105,366 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, AppUtil.Instance.RequestSystemOnValueChanged(0.0f); // Allow the player to receive packets - netPeers.Add(peerId, peer); + peers.Add(serverPlayer.PlayerId, peer); // Send the new player to all other players - ServerPlayer serverPlayer = serverPlayers[peerId]; - ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() { - Id = peerId, + ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() + { + PlayerId = serverPlayer.PlayerId, Username = serverPlayer.Username, - Guid = serverPlayer.Guid.ToByteArray() + //Guid = serverPlayer.Guid.ToByteArray() }; SendPacketToAll(clientboundPlayerJoinedPacket, DeliveryMethod.ReliableOrdered, peer); + LogDebug(() => $"Chatmanager"); + ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, serverPlayer); + Log($"Client {peer.Id} is ready. Sending world state"); // No need to sync the world state if the player is the host - if (NetworkLifecycle.Instance.IsHost(peer)) + if (NetworkLifecycle.Instance.IsHost(serverPlayer)) { SendPacket(peer, new ClientboundRemoveLoadingScreenPacket(), DeliveryMethod.ReliableOrdered); + serverPlayer.IsLoaded = true; + PlayerReady?.Invoke(serverPlayer); return; } SendPacket(peer, new ClientboundBeginWorldSyncPacket(), DeliveryMethod.ReliableOrdered); // Send weather state - SendPacket(peer, WeatherDriver.Instance.GetSaveData().ToObject(), DeliveryMethod.ReliableOrdered); + SendWeatherState(peer); // 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]], peer); + } + } + else + { + LogError($"Sending job packets... Failed to get NetworkedStation from station"); + } } // Send existing players foreach (ServerPlayer player in ServerPlayers) { - if (player.Id == peer.Id) + if (player.PlayerId == serverPlayer.PlayerId) continue; - SendPacket(peer, new ClientboundPlayerJoinedPacket { - Id = player.Id, + SendPacket(peer, new ClientboundPlayerJoinedPacket + { + PlayerId = player.PlayerId, 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; + + PlayerReady?.Invoke(serverPlayer); } - private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, NetPeer peer) + private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, ITransportPeer peer) { - if (TryGetServerPlayer(peer, out ServerPlayer player)) + if (!TryGetServerPlayer(peer, out ServerPlayer player)) { - player.RawPosition = packet.Position; - player.RawRotationY = packet.RotationY; + LogWarning($"Received Player Position from {peer.GetType()}, peerId: {peer.Id}, but could not find matching player."); + return; } - ClientboundPlayerPositionPacket clientboundPacket = new() { - Id = (byte)peer.Id, + player.CarId = packet.CarID; + player.RawPosition = packet.Position; + player.RawRotationY = packet.RotationY; + + ClientboundPlayerPositionPacket clientboundPacket = new() + { + PlayerId = player.PlayerId, 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) + private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, ITransportPeer peer) + { + SendPacketToAll + ( + new ClientboundTimeAdvancePacket + { + amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds + }, + DeliveryMethod.ReliableUnordered, + peer + ); + } + + private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, ITransportPeer peer) + { + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + + private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, ITransportPeer peer) { - if (packet.CarId != 0 && !NetworkedTrainCar.Get(packet.CarId, out NetworkedTrainCar _)) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + + private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, ITransportPeer peer) + { + if (!peerToPlayer.TryGetValue(peer, out var player)) + { + LogWarning($"Received Coupler Interaction from {peer.GetType()}, peerId: {peer.Id}, but could not find matching player."); return; + } - if (TryGetServerPlayer(peer, out ServerPlayer player)) - player.CarId = packet.CarId; + //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.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + { + if (netTrainCar.Server_ValidateCouplerInteraction(packet, player)) + { + //passed validation, send to all but the originator + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + else + { + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.PlayerId}) Sending validation failure"); + //failed validation notify client + SendPacket + ( + peer, + new CommonCouplerInteractionPacket + { + NetId = packet.NetId, + Flags = (ushort)CouplerInteractionType.NoAction, + IsFrontCoupler = packet.IsFrontCoupler, + }, + DeliveryMethod.ReliableOrdered + ); + } + } + else + { + LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {player.PlayerId}) Sending destroy"); + //Car doesn't exist, tell client to delete it + SendDestroyTrainCar(netTrainCar, peer); + } + } - ClientboundPlayerCarPacket clientboundPacket = new() { - Id = (byte)peer.Id, - CarId = packet.CarId - }; + //private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) + //{ + // SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + //} - SendPacketToAll(clientboundPacket, DeliveryMethod.ReliableOrdered, peer); + private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, ITransportPeer peer) + { + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) + private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(new ClientboundTimeAdvancePacket { - amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds - }, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, NetPeer peer) + private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, NetPeer peer) + private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, NetPeer peer) + private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, NetPeer peer) + private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, NetPeer peer) + private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, NetPeer peer) + private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, NetPeer peer) + private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar netTrainCar)) + return; + + if (!PaintThemeLookup.Instance.TryGet(packet.PaintThemeId, out PaintTheme paint) || paint == null) + { + LogWarning($"Received paint theme change for {netTrainCar?.CurrentID}, but paint theme id '{packet.PaintThemeId}' does not exist."); + return; + } + + Log($"Received paint theme change for {netTrainCar?.CurrentID}, theme '{paint.AssetName}'"); + + LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {packet.TargetArea}, paint: [{paint?.AssetName}, {packet.PaintThemeId}]"); + + netTrainCar?.Server_ValidatePaintThemeChange(packet.TargetArea, paint, player); + + //SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, NetPeer peer) + private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + //is value valid? + if (float.IsNaN(packet.CoalMassDelta)) + return; + + if (!NetworkLifecycle.Instance.IsHost(player)) + { + //is player close enough to add coal? + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= networkedTrainCar.CarLengthSq) + networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); + } } - private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, NetPeer peer) + private void OnServerboundTenderCoalPacket(ServerboundTenderCoalPacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + // is value valid? + if (float.IsNaN(packet.CoalMassDelta)) + return; + + if (!NetworkLifecycle.Instance.IsHost(player)) + { + //is player close enough to add/remove coal? + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= networkedTrainCar.CarLengthSq) + networkedTrainCar.coalPile?.coalConsumePort.ExternalValueUpdate(packet.CoalMassDelta); + } } - private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, NetPeer peer) + private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, ITransportPeer peer) { - SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + if (!NetworkLifecycle.Instance.IsHost(player)) + { + //is player close enough to ignite firebox? + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= networkedTrainCar.CarLengthSq) + networkedTrainCar.firebox?.Ignite(); + } } - private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, NetPeer peer) + private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, ITransportPeer peer) { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + //if not the host && validation fails then ignore packet + if (!NetworkLifecycle.Instance.IsHost(player)) + { + 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 OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer peer) + private void OnServerboundTrainControlAuthorityPacket(ServerboundTrainControlAuthorityPacket 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 (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + networkedTrainCar.Server_ReceiveAuthorityRequest(packet.PortNetId, player, packet.RequestAuthority); } - private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, NetPeer peer) + private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } private void OnServerboundTrainSyncRequestPacket(ServerboundTrainSyncRequestPacket packet) { - if (NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) networkedTrainCar.Server_DirtyAllState(); } - private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequestPacket packet, NetPeer peer) + private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; if (networkedTrainCar.HasPlayers) @@ -577,7 +1481,7 @@ private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequest return; } - Job job = JobsManager.Instance.GetJobOfCar(trainCar); + Job job = JobsManager.Instance.GetJobOfCar(trainCar.logicCar); switch (job?.State) { case JobState.Available: @@ -591,18 +1495,22 @@ 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; - if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + if (!NetworkedTrainCar.TryGet(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - if (!NetworkedRailTrack.Get(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) + if (!NetworkedRailTrack.TryGet(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) return; 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 +1520,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; @@ -647,6 +1555,196 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas else 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 OnServerboundWarehouseMachineControllerRequestPacket(ServerboundWarehouseMachineControllerRequestPacket packet, ITransportPeer peer) + { + LogDebug(() => $"ServerboundWarehouseMachineControllerRequestPacket(): {packet.NetId}"); + + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + { + LogWarning($"ServerboundWarehouseMachineControllerRequestPacket() ServerPlayer not found: {peer.Id}"); + return; + } + + //Todo: add check for player authorisation to use loading/uloading machines + + //Find the warehouse + if (!NetworkedWarehouseMachineController.Get(packet.NetId, out var targetWarehouse)) + { + LogWarning($"ServerboundWarehouseMachineControllerRequestPacket() WarehouseMachineController not found. NetId: {packet.NetId}"); + return; + } + + //Todo: add check for player distance from machine + + targetWarehouse.ServerProcessWarehouseAction(packet.WarehouseAction); + } + + private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) + { + if (TryGetServerPlayer(peer, out ServerPlayer player)) + ChatManager.ProcessMessage(packet.message, player); + } + #endregion + + #region Unconnected Packet Handling + private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) + { + //Log($"OnUnconnectedPingPacket({endPoint.Address})"); + //SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); + } + + private void OnCommonPitStopInteractionPacket(CommonPitStopInteractionPacket packet, ITransportPeer peer) + { + bool foundPlayer = TryGetServerPlayer(peer, out var player); + if (!foundPlayer) + { + LogWarning($"Received Pit Stop Plug Interaction, but player was not found"); + } + else + { + if (NetworkedPitStopStation.Get(packet.NetId, out NetworkedPitStopStation controller)) + controller.ProcessInteractionPacketAsHost(packet, player); + else + LogWarning($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); + } + } + + private void OnCommonPitStopPlugInteractionPacket(CommonPitStopPlugInteractionPacket packet, ITransportPeer peer) + { + bool foundPlayer = TryGetServerPlayer(peer, out var player); + if (!foundPlayer) + { + LogWarning($"Received Pit Stop Plug Interaction, but player was not found"); + SendNetSerializablePacket(peer, new CommonPitStopPlugInteractionPacket + { + NetId = packet.NetId, + InteractionType = (byte)PitStopStationInteractionType.Reject + }, DeliveryMethod.ReliableOrdered); + } + + if (NetworkedPluggableObject.Get(packet.NetId, out NetworkedPluggableObject plug) && foundPlayer) + { + plug.ProcessInteractionPacketAsHost(packet, player); + } + else + { + LogError($"OnCommonPitStopInteractionPacket() Failed to find PitStopStation with netId: {packet.NetId}"); + } + } + + private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportPeer peer) + { + //if(!TryGetServerPlayer(peer, out var player)) + // return; + + //LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id} (\"{player.Username}\"))"); + + //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); + } + + private void OnCommonCashRegisterWithModulesActionPacket(CommonCashRegisterWithModulesActionPacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out var player)) + { + LogWarning($"Cash Register With Modules Action received, but player was not found"); + return; + } + + if (!NetworkedCashRegisterWithModules.Get(packet.NetId, out NetworkedCashRegisterWithModules netCashRegister)) + { + LogWarning($"Cash Register With Modules Action received for netId: {packet.NetId}, but cash register does not exist!"); + return; + } + + Log($"Cash Register With Modules Action received for {netCashRegister.GetObjectPath()}, Action: {packet.Action}, Amount: {packet.Amount}"); + netCashRegister.Server_ProcessCashRegisterAction(player, packet); + } + + private void OnCommonGenericSwitchStatePacket(CommonGenericSwitchStatePacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out var player)) + { + LogWarning($"Received Generic Switch State, but player was not found"); + return; + } + + if (!NetworkedGenericSwitch.TryGet(packet.NetId, out NetworkedGenericSwitch netSwitch)) + { + LogWarning($"Received Generic Switch State from \"{player.Username}\" for switch {packet.NetId}, but switch does not exist!"); + return; + } + + netSwitch.Server_ReceiveSwitchState(packet.IsOn, player); + } #endregion } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs new file mode 100644 index 00000000..b39519d5 --- /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/ClientboundLoginResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs new file mode 100644 index 00000000..c29327a6 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs @@ -0,0 +1,13 @@ +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Clientbound; + +public class ClientboundLoginResponsePacket +{ + public bool Accepted { get; set; } + public byte PlayerId { get; set; } + public string ReasonKey { get; set; } + public string[] ReasonArgs { get; set; } + public ModInfo[] Missing { get; set; } = []; + public ModInfo[] Extra { get; set; } = []; +} diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs index 7fed6121..6eec4cbd 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPingUpdatePacket.cs @@ -2,6 +2,6 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPingUpdatePacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } public int Ping { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs deleted file mode 100644 index 10c0c4ce..00000000 --- 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/ClientboundPlayerDisconnectPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs index 2035379e..ac1732f0 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerDisconnectPacket.cs @@ -2,5 +2,5 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPlayerDisconnectPacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs index 409c3f5d..806ddfc9 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs @@ -4,10 +4,10 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPlayerJoinedPacket { - public byte Id { get; set; } + public byte PlayerId { 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 bf27e5a9..65144a88 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs @@ -4,11 +4,12 @@ namespace Multiplayer.Networking.Packets.Clientbound; public class ClientboundPlayerPositionPacket { - public byte Id { get; set; } + public byte PlayerId { get; set; } public Vector3 Position { get; set; } 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 6ecf96d3..b6a2a610 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs @@ -1,5 +1,6 @@ using DV.InventorySystem; using DV.JObjectExtstensions; +using DV.Logic.Job; using DV.ServicePenalty; using DV.UserManagement; using Multiplayer.Components.Networking; @@ -31,8 +32,11 @@ public class ClientboundSaveGameDataPacket // public string Debt_deleted_jobless_cars { get; set; } // public string Debt_insurance { get; set; } + public float JobManagerTime { get; set; } + public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) { + Multiplayer.LogDebug(() => $"ClientboundSaveGameDataPacket.CreatePacket() for player (is null: {player == null}) {player?.Username} ({player?.Guid})"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); @@ -43,6 +47,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), @@ -52,7 +67,7 @@ public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) UnlockedGarages = data.GetStringArray(SaveGameKeys.Garages), Position = playerData?.GetVector3(SaveGameKeys.Player_position) ?? LevelInfo.DefaultSpawnPosition, Rotation = playerData?.GetFloat(SaveGameKeys.Player_rotation) ?? LevelInfo.DefaultSpawnRotation.y, - HasDebt = data.GetFloat(SaveGameKeys.Debt_total).GetValueOrDefault(CareerManagerDebtController.Instance != null ? CareerManagerDebtController.Instance.NumberOfNonZeroPricedDebts : 0) > 0 + HasDebt = data.GetFloat(SaveGameKeys.Debt_total).GetValueOrDefault(CareerManagerDebtController.Instance != null ? CareerManagerDebtController.Instance.NumberOfNonZeroPricedDebts : 0) > 0, // Debt_existing_locos = data.GetJObjectArray(SaveGameKeys.Debt_existing_locos)?.NotNull().Select(j => j.ToString()).ToArray(), // Debt_deleted_locos = data.GetJObjectArray(SaveGameKeys.Debt_deleted_locos)?.NotNull().Select(j => j.ToString()).ToArray(), // Debt_existing_jobs = data.GetJObjectArray(SaveGameKeys.Debt_existing_jobs)?.NotNull().Select(j => j.ToString()).ToArray(), @@ -60,6 +75,8 @@ public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) // Debt_existing_jobless_cars = data.GetJObject(SaveGameKeys.Debt_existing_jobless_cars)?.ToString(), // Debt_deleted_jobless_cars = data.GetJObject(SaveGameKeys.Debt_deleted_jobless_cars)?.ToString(), // Debt_insurance = data.GetJObject(SaveGameKeys.Debt_insurance)?.ToString() + + JobManagerTime = JobsManager.Instance.Time }; } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs deleted file mode 100644 index 4f77ed32..00000000 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Multiplayer.Networking.Data; - -namespace Multiplayer.Networking.Packets.Clientbound; - -public class ClientboundServerDenyPacket -{ - public string ReasonKey { get; set; } - public string[] ReasonArgs { get; set; } - public ModInfo[] Missing { get; set; } = Array.Empty(); - public ModInfo[] Extra { get; set; } = Array.Empty(); -} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs new file mode 100644 index 00000000..e489af20 --- /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 00000000..7598ac52 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -0,0 +1,28 @@ +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using System.Collections.Generic; + +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); + 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 00000000..b6f8bbe1 --- /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/Jobs/ClientboundTaskUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs new file mode 100644 index 00000000..abf7ff5d --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundTaskUpdatePacket.cs @@ -0,0 +1,11 @@ +using DV.Logic.Job; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +internal class ClientboundTaskUpdatePacket +{ + public ushort TaskNetId { get; set; } + public TaskState NewState { get; set; } + public float TaskStartTime { get; set; } + public float TaskFinishTime { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs new file mode 100644 index 00000000..42d6f545 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundWarehouseControllerUpdatePacket.cs @@ -0,0 +1,12 @@ + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundWarehouseControllerUpdatePacket +{ + public ushort NetId { get; set; } + public bool IsLoading { get; set; } + public ushort JobNetId { get; set; } + public ushort CarNetId { get; set; } + public uint CargoTypeNetId { get; set; } + public ushort Preset { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs new file mode 100644 index 00000000..d75f5713 --- /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/ClientboundCarHealthUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs index dd6846d6..1bcfdf70 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCarHealthUpdatePacket.cs @@ -1,7 +1,9 @@ +using Multiplayer.Networking.Data.Train; + namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundCarHealthUpdatePacket { public ushort NetId { get; set; } - public float Health { get; set; } + public TrainCarHealthData Health { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs new file mode 100644 index 00000000..ac6290ce --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoHealthUpdatePacket.cs @@ -0,0 +1,7 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboundCargoHealthUpdatePacket +{ + public ushort NetId { get; set; } + public float CargoHealth { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs index a37f301e..b9d11c2f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundCargoStatePacket.cs @@ -1,11 +1,15 @@ +using DV.ThingTypes; +using System; + namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundCargoStatePacket { public ushort NetId { get; set; } public bool IsLoading { get; set; } - public ushort CargoType { get; set; } + public uint CargoTypeNetId { get; set; } public float CargoAmount { get; set; } + public float CargoHealth { get; set; } public byte CargoModelIndex { get; set; } - public string WarehouseMachineId { get; set; } + public ushort WarehouseMachineNetId { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs index 122de31e..0d69e5ff 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 e81d5356..6d9d3968 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/ClientboundTrainControlAuthorityUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainControlAuthorityUpdatePacket.cs new file mode 100644 index 00000000..ee14656f --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainControlAuthorityUpdatePacket.cs @@ -0,0 +1,18 @@ + +using DV.HUD; + +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public enum ControlAuthorityState : byte +{ + Released, + Blocked, + Denied +} + +public class ClientboundTrainControlAuthorityUpdatePacket +{ + public ushort NetId { get; set; } + public uint PortNetId { get; set; } + public ControlAuthorityState State { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs index aee1b0f2..05903fae 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/ClientboundPitStopBulkUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs new file mode 100644 index 00000000..0043253f --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundPitStopBulkUpdatePacket.cs @@ -0,0 +1,17 @@ +using Multiplayer.Networking.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Multiplayer.Networking.Packets.Clientbound.World; + +public class ClientboundPitStopBulkUpdatePacket +{ + public ushort NetId { get; set; } + public int CarCount { get; set; } + public int CarSelection { get; set; } + public int FaucetNotch { get; set; } + public LocoResourceModuleData[] ResourceData { get; set; } + public PitStopPlugData[] PlugData { 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 00000000..786fdc2e --- /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/CommonCashRegisterWithModulesActionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs new file mode 100644 index 00000000..780a0811 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonCashRegisterWithModulesActionPacket.cs @@ -0,0 +1,20 @@ + +namespace Multiplayer.Networking.Packets.Common; + +public enum CashRegisterAction : byte +{ + Cancel, + Buy, + AddCash, + SetFunds, + RejectGeneric, + RejectFunds, + RejectedNoItems, + Approve +} +public class CommonCashRegisterWithModulesActionPacket +{ + public ushort NetId { get; set; } + public CashRegisterAction Action { get; set; } + public double Amount { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs new file mode 100644 index 00000000..1c511ad8 --- /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/CommonGenericSwitchStatePacket.cs b/Multiplayer/Networking/Packets/Common/CommonGenericSwitchStatePacket.cs new file mode 100644 index 00000000..6d0e1ca0 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonGenericSwitchStatePacket.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonGenericSwitchStatePacket +{ + public uint NetId { get; set; } + public bool IsOn { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs new file mode 100644 index 00000000..0445a11c --- /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/CommonPitStopInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs new file mode 100644 index 00000000..40aeaff0 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopInteractionPacket.cs @@ -0,0 +1,13 @@ + +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonPitStopInteractionPacket +{ + public uint Tick { get; set; } + public ushort NetId { get; set; } + public PitStopStationInteractionType InteractionType { get; set; } + public int ResourceType { get; set; } + public float Value { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs new file mode 100644 index 00000000..5d13ca4a --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonPitStopPlugInteractionPacket.cs @@ -0,0 +1,94 @@ + +using LiteNetLib.Utils; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Serialization; +using UnityEngine; + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonPitStopPlugInteractionPacket : INetSerializable +{ + public ushort NetId { get; set; } + public PlugInteractionType InteractionType { get; set; } + public byte PlayerId { get; set; } + public ushort TrainCarNetId { get; set; } + public sbyte SocketIndex { get; set; } + public Vector3? Position { get; set; } + public Quaternion? Rotation { get; set; } + public Vector3? YankForce { get; set; } + public ForceMode YankMode { get; set; } + + public void Deserialize(NetDataReader reader) + { + NetId = reader.GetUShort(); + InteractionType = (PlugInteractionType)reader.GetByte(); + + switch (InteractionType) + { + case PlugInteractionType.Rejected: + break; + + case PlugInteractionType.PickedUp: + PlayerId = reader.GetByte(); + break; + + case PlugInteractionType.Dropped: + Position = Vector3Serializer.Deserialize(reader); + Rotation = QuaternionSerializer.Deserialize(reader); + break; + + case PlugInteractionType.Yanked: + Position = Vector3Serializer.Deserialize(reader); + Rotation = QuaternionSerializer.Deserialize(reader); + + YankForce = Vector3Serializer.Deserialize(reader); + YankMode = (ForceMode)reader.GetByte(); + break; + + case PlugInteractionType.DockHome: + break; + + case PlugInteractionType.DockSocket: + TrainCarNetId = reader.GetUShort(); + SocketIndex = reader.GetSByte(); + break; + } + } + + public void Serialize(NetDataWriter writer) + { + writer.Put(NetId); + writer.Put((byte)InteractionType); + + switch (InteractionType) + { + case PlugInteractionType.Rejected: + break; + + case PlugInteractionType.PickedUp: + writer.Put(PlayerId); + break; + + case PlugInteractionType.Dropped: + Vector3Serializer.Serialize(writer, Position ?? Vector3.zero); + QuaternionSerializer.Serialize(writer, Rotation ?? Quaternion.identity); + break; + + case PlugInteractionType.Yanked: + Vector3Serializer.Serialize(writer, Position ?? Vector3.zero); + QuaternionSerializer.Serialize(writer, Rotation ?? Quaternion.identity); + + Vector3Serializer.Serialize(writer, YankForce ?? Vector3.zero); + writer.Put((byte)YankMode); + break; + + case PlugInteractionType.DockHome: + break; + + case PlugInteractionType.DockSocket: + writer.Put(TrainCarNetId); + writer.Put(SocketIndex); + break; + } + } +} diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs new file mode 100644 index 00000000..8766f280 --- /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 00000000..a4423bb4 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs @@ -0,0 +1,10 @@ +using DV.Customization.Paint; + +namespace Multiplayer.Networking.Packets.Common.Train; + +public class CommonPaintThemePacket +{ + public ushort NetId { get; set; } + public TrainCarPaint.Target TargetArea { get; set; } + public uint PaintThemeId { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs index 36005897..ce04a4c3 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/CommonTrainFusesPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs index 4b723a35..5550bbdc 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainFusesPacket.cs @@ -1,8 +1,8 @@ -namespace Multiplayer.Networking.Packets.Common.Train; +namespace Multiplayer.Networking.Packets.Common.Train; public class CommonTrainFusesPacket { public ushort NetId { get; set; } - public string[] FuseIds { get; set; } + public uint[] FuseIds { get; set; } public bool[] FuseValues { get; set; } } diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs index 649cff84..c23720a1 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainPortsPacket.cs @@ -1,8 +1,8 @@ -namespace Multiplayer.Networking.Packets.Common.Train; +namespace Multiplayer.Networking.Packets.Common.Train; public class CommonTrainPortsPacket { public ushort NetId { get; set; } - public string[] PortIds { get; set; } + public uint[] PortIds { get; set; } public float[] PortValues { get; set; } } diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs index 590c7b0a..de543396 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 00000000..8e51f85a --- /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/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs new file mode 100644 index 00000000..f7fc7515 --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundWarehouseMachineControllerRequestPacket.cs @@ -0,0 +1,9 @@ +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Serverbound.Jobs; + +public class ServerboundWarehouseMachineControllerRequestPacket +{ + public ushort NetId { get; set; } + public WarehouseAction WarehouseAction { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs index 9d76d2ee..0f725dac 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundClientLoginPacket.cs @@ -7,6 +7,6 @@ public class ServerboundClientLoginPacket public string Username { get; set; } public byte[] Guid { get; set; } public string Password { get; set; } - public ushort BuildMajorVersion { get; set; } + public string BuildVersion { get; set; } public ModInfo[] Mods { get; set; } } diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs deleted file mode 100644 index 8ca39e93..00000000 --- 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 b4f1f3c6..c13d1417 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 00000000..a3a17637 --- /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 00000000..c9ededb7 --- /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/Train/ServerboundTenderCoalPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTenderCoalPacket.cs new file mode 100644 index 00000000..2efe682b --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTenderCoalPacket.cs @@ -0,0 +1,7 @@ +namespace Multiplayer.Networking.Packets.Serverbound.Train; + +public class ServerboundTenderCoalPacket +{ + public ushort NetId { get; set; } + public float CoalMassDelta { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs new file mode 100644 index 00000000..316d982f --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainControlAuthorityPacket.cs @@ -0,0 +1,12 @@ +using DV.HUD; + +namespace Multiplayer.Networking.Packets.Serverbound.Train; + +public class ServerboundTrainControlAuthorityPacket +{ + public ushort NetId { get; set; } + public uint PortNetId { get; set; } + public InteriorControlsManager.ControlType ControlType { get; set; } + public bool RequestAuthority { 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 bcf2023b..0de0e6ea 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 8b5da122..62aefade 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 be4950ed..91ec9dcc 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 00000000..2c01c51d --- /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 00000000..5ee21597 --- /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/ColorSerializer.cs b/Multiplayer/Networking/Serialization/ColorSerializer.cs new file mode 100644 index 00000000..1493cd91 --- /dev/null +++ b/Multiplayer/Networking/Serialization/ColorSerializer.cs @@ -0,0 +1,22 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Networking.Serialization +{ + public class ColorSerializer + { + public static void Serialize(NetDataWriter writer, Color colour) + { + writer.Put(colour.ColorToUInt32()); + } + + public static Color Deserialize(NetDataReader reader) + { + var colour = reader.GetUInt(); + + return colour.UInt32ToColor(); + } + } + +} diff --git a/Multiplayer/Networking/Serialization/QuaternionSerializer.cs b/Multiplayer/Networking/Serialization/QuaternionSerializer.cs new file mode 100644 index 00000000..cd95a4de --- /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 00000000..39c4ec12 --- /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 00000000..ab8209c3 --- /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 00000000..796db2e2 --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -0,0 +1,442 @@ +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; +using UnityEngine; + + +namespace Multiplayer.Networking.TransportLayers; + +public class SteamWorksTransport : ITransport +{ + public NetStatistics Statistics => new(); + 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) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port}) Relay not 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); + + foreach (var server in servers) + { + if (server != null) + { + // Close all connections first + foreach (var connection in server.Connected) + { + connection.Close(true, (int)NetConnectionEnd.App_Generic); + } + + //close the server + server.Close(); + } + } + + servers.Clear(); + } + + 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) + { + float chance = 0f; + if (settings.SimulatePacketLoss) + chance = settings.SimulationPacketLossChance; + + SteamNetworkingUtils.FakeRecvPacketLoss = chance; + SteamNetworkingUtils.FakeSendPacketLoss = chance; + + + chance = 0; + if (settings.SimulateLatency) + chance = UnityEngine.Random.Range(settings.SimulationMinLatency, settings.SimulationMaxLatency); + + SteamNetworkingUtils.FakeRecvPacketLag = chance; + SteamNetworkingUtils.FakeSendPacketLag = chance; + } + + #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 0cd194a3..e7b56046 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,11 +42,11 @@ 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. - __instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.Instance.originShiftParent); + __instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.OriginShiftParent); CommsRadioController.PlayAudioFromRadio(__instance.confirmSound, __instance.transform); } @@ -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 00000000..092e5874 --- /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 00000000..1d20128f --- /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 06c4ab73..3534dab1 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 00000000..9bccf2dc --- /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 00000000..6dc12f1a --- /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 00000000..c5ba1565 --- /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 00000000..be3f6a4e --- /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 00000000..8ab6af3e --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -0,0 +1,152 @@ +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 +{ + private const float TIME_OUT = 3f; + + [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) + { + Multiplayer.LogDebug(() => $"Awaiting validation response for {networkedJob?.Job?.ID}..."); + + float timeout = Time.time; + + //Book spawns can take a few seconds, this may be due to how the asset is loaded and rendered + yield return new WaitUntil + ( + () => + { + return networkedJob.ValidatorResponseReceived || (Time.time - timeout > TIME_OUT); + } + ); + + //WaitForSecondsRealtime(Math.Max(4f,(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 00000000..dc89c2d3 --- /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 00000000..e77279f6 --- /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 00000000..add95b29 --- /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 217630b5..0d82e62d 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/Jobs/TaskPatch.cs b/Multiplayer/Patches/Jobs/TaskPatch.cs new file mode 100644 index 00000000..796c43bf --- /dev/null +++ b/Multiplayer/Patches/Jobs/TaskPatch.cs @@ -0,0 +1,42 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using System; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(Task))] +public static class TaskPatch +{ + [HarmonyPatch(nameof(Task.SetState))] + [HarmonyPrefix] + public static void SetStatePrefix(Task __instance, TaskState newState) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (newState == TaskState.InProgress) + return; + + + //Multiplayer.LogDebug(()=>$"Task.SetState() called for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}, newState: {newState}"); + if(!NetworkedTask.TryGet(__instance, out var networkedTask)) + { + Multiplayer.LogError($"Task.SetState() could not find NetworkedTask for jobId: {__instance.Job.ID}, taskType: {__instance.InstanceTaskType}"); + return; + } + + networkedTask.SetState(newState); + } + + [HarmonyPatch(nameof(Task.SetJobBelonging))] + [HarmonyPostfix] + public static void SetJobBelongingPostfix(Task __instance, Job Job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + NetworkedJob.EnqueueTask(__instance, Job); + } +} diff --git a/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs new file mode 100644 index 00000000..dc45d326 --- /dev/null +++ b/Multiplayer/Patches/Jobs/WarehouseMachineControllerPatch.cs @@ -0,0 +1,126 @@ +using DV.Logic.Job; +using DV.ThingTypes; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using static WarehouseMachineController; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(WarehouseMachineController))] +public class WarehouseMachineControllerPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(WarehouseMachineController.Awake))] + public static void Awake(WarehouseMachineController __instance) + { + __instance.gameObject.AddComponent(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(WarehouseMachineController.SetScreen))] + public static void SetScreen(WarehouseMachineController __instance, TextPreset preset, bool isLoading, string jobId, Car car, CargoType_v2 cargoType) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() is host"); + + bool skip = preset switch + { + TextPreset.Idle => true, + TextPreset.TrainInRange => true, + TextPreset.ClearTrainInRange => true, + _ => false + }; + + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() skipping: {skip}"); + if (skip) + return; + + if (!NetworkedWarehouseMachineController.GetFromWarehouseMachineController(__instance, out var netMachine) || netMachine == null) + { + Multiplayer.LogError($"WarehouseMachineControllerPatch.SetScreen(): Failed to get NetworkedWarehouseMachineController for {__instance.warehouseTrackName}"); + return; + } + + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetMachine found"); + + //obtain serialisable info + ushort carNetId = 0; + ushort jobNetId = 0; + + if (car != null) + { + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() car not null"); + var tc = car.TrainCar(); + if (tc == null || !NetworkedTrainCar.TryGetFromTrainCar(tc, out var netTC)) + { + //Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedTrainCar for {car?.ID}"); + return; + } + + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetCar found"); + carNetId = netTC.NetId; + } + + if (!string.IsNullOrEmpty(jobId)) + { + if(!NetworkedJob.TryGetFromJobId(jobId, out var netJob)) + { + Multiplayer.LogWarning($"WarehouseMachineControllerPatch.SetScreen() Failed to get NetworkedJob for {jobId}"); + return; + } + + //Multiplayer.LogDebug(() => $"WarehouseMachineControllerPatch.SetScreen() NetJob found"); + jobNetId = netJob.NetId; + } + + CargoTypeLookup.Instance.TryGetNetId(cargoType, out uint cargoTypeNetId); + + NetworkLifecycle.Instance.Server.SendWarehouseControllerUpdate(netMachine.NetId, isLoading, jobNetId, carNetId, cargoTypeNetId, preset); + } + + [HarmonyPrefix] + [HarmonyPatch("StartUnloadSequence")] + public static bool StartUnloadSequence_Prefix(WarehouseMachineController __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + SendValidationRequest(__instance, WarehouseAction.Unload); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch("StartLoadSequence")] + public static bool StartLoadSequence_Prefix(WarehouseMachineController __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + SendValidationRequest(__instance, WarehouseAction.Load); + return false; + } + + private static void SendValidationRequest(WarehouseMachineController machine, WarehouseAction action) + { + string id = machine?.warehouseMachine?.ID; + + if (string.IsNullOrEmpty(id)) + { + NetworkLifecycle.Instance.Client.LogError($"Failed to validate {action} for {machine?.name} at {machine?.warehouseTrackName}. Warehouse not found!"); + return; + } + + if (!NetworkedWarehouseMachineController.GetFromWarehouseMachineController(machine, out var netController) || netController == null) + { + NetworkLifecycle.Instance.Client.LogError($"Failed to find NetworkedWarehouseMachineController {machine?.warehouseTrackName}. Warehouse not found!"); + return; + } + + NetworkLifecycle.Instance.Client.SendWarehouseRequest(action, netController.NetId); + } +} diff --git a/Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs b/Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs new file mode 100644 index 00000000..14a115c1 --- /dev/null +++ b/Multiplayer/Patches/Jobs/WarehouseMachinePatch.cs @@ -0,0 +1,17 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking.Jobs; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(WarehouseMachine))] +public class WarehouseMachinePatch +{ + [HarmonyPatch(nameof(WarehouseMachine.ID))] + [HarmonyPatch(MethodType.Setter)] + [HarmonyPostfix] + public static void ID_Set(WarehouseMachine __instance) + { + WarehouseMachineLookup.Instance.RegisterWarehouseMachine( __instance ); + } +} diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs new file mode 100644 index 00000000..9de50908 --- /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 0f799cbf..ceec2547 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 == null || !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 be049356..149a5611 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -1,4 +1,4 @@ -using DV.Localization; +using DV.Localization; using DV.UI; using HarmonyLib; using Multiplayer.Utils; @@ -7,37 +7,62 @@ namespace Multiplayer.Patches.MainMenu; -[HarmonyPatch(typeof(MainMenuController), "Awake")] -public static class MainMenuController_Awake_Patch +/// +/// Harmony patch MainMenuController to add a Multiplayer button. +/// +[HarmonyPatch(typeof(MainMenuController))] +public static class MainMenuControllerPatch { - public static GameObject MultiplayerButton; + public static AMainMenuProvider MenuProvider => MainMenuControllerInstance.provider; + public static GameObject MultiplayerButton { get; private set; } + public static MainMenuController MainMenuControllerInstance { get; private set; } - private static void Prefix(MainMenuController __instance) + /// + /// Prefix method to run before MainMenuController's Awake method. + /// + /// The instance of MainMenuController. + [HarmonyPatch(typeof(MainMenuController), nameof(MainMenuController.Awake))] + [HarmonyPrefix] + private static void Awake(MainMenuController __instance) { - GameObject button = __instance.FindChildByName("ButtonSelectable Sessions"); - if (button == null) + MainMenuControllerInstance = __instance; + + // 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); + // Configure the new Multiplayer button + MultiplayerButton.transform.SetSiblingIndex(sessionsButton.transform.GetSiblingIndex() + 1); MultiplayerButton.name = "ButtonSelectable Multiplayer"; + // 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. + // Remove existing localization components to reset them Object.Destroy(MultiplayerButton.GetComponentInChildren()); - UIElementTooltip tooltip = MultiplayerButton.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = null; + MultiplayerButton.ResetTooltip(); + + // Set the icon for the new Multiplayer button + SetButtonIcon(MultiplayerButton); + } - GameObject icon = MultiplayerButton.FindChildByName("icon"); + /// + /// 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!"); diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 33467b43..74c16e0e 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 = MainMenuControllerPatch.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 + MainMenuControllerPatch.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")) + __instance.StartCoroutine(SteamworksUtils.JoinFromCommandLine()); } } diff --git a/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs b/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs index 72da15ce..98cb07ab 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 00000000..3552affc --- /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 c394305e..65e6ee1b 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,21 +9,27 @@ namespace Multiplayer.Patches.Player; [HarmonyPatch(typeof(CustomFirstPersonController))] public static class CustomFirstPersonControllerPatch { + private const float ROTATION_THRESHOLD = 0.001f; + private static CustomFirstPersonController fps; + private static bool lastOnCar; + private static ushort lastCarNetId; private static Vector3 lastPosition; private static float lastRotationY; private static bool sentFinalPosition; 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; isOnCar = PlayerManager.Car != null; + car = PlayerManager.Car; NetworkLifecycle.Instance.OnTick += OnTick; PlayerManager.CarChanged += OnCarChanged; } @@ -33,6 +40,7 @@ private static void OnDestroy() { if (UnloadWatcher.isQuitting) return; + NetworkLifecycle.Instance.OnTick -= OnTick; PlayerManager.CarChanged -= OnCarChanged; } @@ -40,25 +48,41 @@ 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(); - float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; + if(UnloadWatcher.isUnloading) + return; + + if (isOnCar && car == null) + { + car = PlayerManager.Car; + isOnCar = car != null; + } + + Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.PlayerTransform.GetWorldAbsolutePosition(); + float rotationY = PlayerManager.PlayerCamera.transform.eulerAngles.y; + + ushort carNetID = isOnCar ? car.GetNetId() : (ushort)0; + + bool positionOrRotationChanged = lastOnCar != isOnCar || (isOnCar && (lastCarNetId != carNetID)) || Vector3.Distance(lastPosition, position) > 0 || Math.Abs(lastRotationY - rotationY) > 0.2f;//ROTATION_THRESHOLD; - bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); if (!positionOrRotationChanged && sentFinalPosition) return; + lastOnCar = isOnCar; + lastCarNetId = carNetID; lastPosition = position; lastRotationY = rotationY; sentFinalPosition = !positionOrRotationChanged; - NetworkLifecycle.Instance.Client.SendPlayerPosition(lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, isJumping, isOnCar, isJumping || sentFinalPosition); + + 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 00000000..ffa0f091 --- /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 ebcd4697..00000000 --- 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 89d806c6..0989eb13 100644 --- a/Multiplayer/Patches/Train/BogiePatch.cs +++ b/Multiplayer/Patches/Train/BogiePatch.cs @@ -2,38 +2,58 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; -[HarmonyPatch(typeof(Bogie), nameof(Bogie.SetupPhysics))] -public static class Bogie_SetupPhysics_Patch +[HarmonyPatch(typeof(Bogie))] +public static class BogiePatch { - private static void Postfix(Bogie __instance) + + [HarmonyTranspiler] + [HarmonyPatch(nameof(Bogie.UpdatePointSetTraveller))] + private static IEnumerable UpdatePointSetTraveller(IEnumerable instructions) + { + var codes = new List(instructions); + + // Find the Debug.LogError call and remove it along with its argument preparation + for (int i = 0; i < codes.Count; i++) + { + // Look for the Debug.LogError call + if (codes[i].opcode == OpCodes.Call && + codes[i].operand is MethodInfo method && + method.DeclaringType == typeof(Debug) && + method.Name == nameof(Debug.LogError)) + { + // Remove the 5 instructions that prepare and call LogError: + // ldstr, ldarg.1, box, call String.Format, call Debug.LogError + if (i >= 4) + { + codes.RemoveRange(i - 4, 5); + break; + } + } + } + + return codes; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Bogie.SetupPhysics))] + private static void SetupPhysics(Bogie __instance) { if (!NetworkLifecycle.Instance.IsHost()) __instance.gameObject.GetOrAddComponent(); } -} -[HarmonyPatch(typeof(Bogie), nameof(Bogie.SwitchJunctionIfNeeded))] -public static class Bogie_SwitchJunctionIfNeeded_Patch -{ - private static bool Prefix() + [HarmonyPrefix] + [HarmonyPatch(nameof(Bogie.SwitchJunctionIfNeeded))] + private static bool SwitchJunctionIfNeeded() { 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/CarRollingAudioModulePatch.cs b/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs index eaf86f03..98ed2c54 100644 --- a/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs +++ b/Multiplayer/Patches/Train/CarRollingAudioModulePatch.cs @@ -7,9 +7,9 @@ namespace Multiplayer.Patches.World; [HarmonyPatch(typeof(CarRollingAudioModule), nameof(CarRollingAudioModule.PlayJointAtBogie))] public static class CarRollingAudioModulePatch { - private static bool Prefix() - { - // todo: There's a bug with bogie joint sounds for clients that causes it to play hundreds of times per-frame. Once that's fixed, this patch can be removed. - return NetworkLifecycle.Instance.IsHost(); - } + //private static bool Prefix() + //{ + // // todo: There's a bug with bogie joint sounds for clients that causes it to play hundreds of times per-frame. Once that's fixed, this patch can be removed. + // return NetworkLifecycle.Instance.IsHost(); + //} } diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index 06d2ae42..7aaad68f 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); + } + + //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 7b9b49d6..30ba5cb8 100644 --- a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs +++ b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs @@ -1,22 +1,65 @@ using DV; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(CarVisitChecker))] public static class CarVisitCheckerPatch { [HarmonyPrefix] [HarmonyPatch(nameof(CarVisitChecker.IsRecentlyVisited), MethodType.Getter)] - private static bool IsRecentlyVisited_Prefix(ref bool __result) + public 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 4e705500..70e2f5c5 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 00000000..3fdc86ec --- /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 ffe7b930..64eb310a 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -1,52 +1,49 @@ 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 + +[HarmonyPatch(typeof(Coupler))] +public static class CouplerPatch { - private static void Postfix(Coupler __instance, Coupler other, bool playAudio, bool viaChainInteraction) + [HarmonyPatch(nameof(Coupler.ConnectAirHose))] + [HarmonyPostfix] + private static void ConnectAirHose(Coupler __instance, Coupler other, bool playAudio) { + //Multiplayer.LogDebug(() => $"ConnectAirHose([{__instance?.train?.ID}, isFront: {__instance?.isFrontCoupler}])\r\n{new System.Diagnostics.StackTrace()}"); + 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 -{ - private static void Postfix(Coupler __instance, bool playAudio, bool calledOnOtherCoupler, bool dueToBrokenCouple, bool viaChainInteraction) - { - if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket || calledOnOtherCoupler) - return; - if (!__instance.train.TryNetworked(out NetworkedTrainCar networkedTrainCar) || networkedTrainCar.IsDestroying) + //Ensure local car has initialised and brakes have been connected on spawn before sending any packets + if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !(netTrainCar?.Client_Initialized ?? false) || netTrainCar?.gameObject == null) + { + Multiplayer.LogWarning($"ConnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, GO exists: {netTrainCar?.gameObject != null}, Initialised: {netTrainCar?.Client_Initialized}"); return; - NetworkLifecycle.Instance.Client?.SendTrainUncouple(__instance, playAudio, dueToBrokenCouple, viaChainInteraction); - } -} + } -[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); + + //Ensure local car has initialised and brakes have been connected on spawn before sending any packets + if (!NetworkedTrainCar.TryGetFromTrainCar(__instance?.train, out var netTrainCar) || !(netTrainCar?.Client_Initialized ?? false) || netTrainCar?.gameObject == null) + { + Multiplayer.LogWarning($"DisconnectAirHose({__instance?.train?.ID}) netTrainCar found: {netTrainCar != null}, GO exists: {netTrainCar?.gameObject != null}, Initialised: {netTrainCar?.Client_Initialized}"); + return; + } + + Multiplayer.LogDebug(() => $"DisconnectAirHose({__instance?.train?.ID}, {__instance.isFrontCoupler})"); + 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 00000000..ee87e61f --- /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 e2b68fb5..62d33fe6 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 00000000..f0243e0c --- /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 edb9dbfa..90f752dd 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/ScriptStripperRuntimePatch.cs b/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs new file mode 100644 index 00000000..57108a6b --- /dev/null +++ b/Multiplayer/Patches/Train/ScriptStripperRuntimePatch.cs @@ -0,0 +1,60 @@ +using DV.Optimizers; +using HarmonyLib; +using Multiplayer.Components.Networking; +using UnityEngine; + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(ScriptStripperRuntime))] +public static class ScriptStripperRuntimePatch +{ + [HarmonyPatch(nameof(ScriptStripperRuntime.Strip))] + [HarmonyPrefix] + public static bool Strip(GameObject goToStrip) + { + if(!NetworkLifecycle.Instance.IsHost()) + return true; + + var trainCar = TrainCar.Resolve(goToStrip); + + if (trainCar == null) + return true; + + MonoBehaviour[] scripts = goToStrip.GetComponentsInChildren(); + Joint[] joints = goToStrip.GetComponentsInChildren(); + Rigidbody[] rigidBodies = goToStrip.GetComponentsInChildren(); + Collider[] colliders = goToStrip.GetComponentsInChildren(); + + for (int i = 0; i < joints.Length; i++) + { + Object.Destroy(joints[i]); + } + + for (int i = 0; i < rigidBodies.Length; i++) + { + Object.Destroy(rigidBodies[i]); + } + + for (int i = 0; i < colliders.Length; i++) + { + if(!colliders[i].TryGetComponent(out _)) + Object.Destroy(colliders[i]); + //else + //{ + // Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping collider {colliders[i].gameObject.GetPath()} for {trainCar.ID}, has LocoResourceReceiver component."); + //} + } + + for (int i = 0; i < scripts.Length; i++) + { + if (!scripts[i].GetType().Equals(typeof(LocoResourceReceiver))) + Object.Destroy(scripts[i]); + //else + //{ + // Multiplayer.LogDebug(() => $"ScriptStripperRuntimePatch.Strip() Keeping script {scripts[i].gameObject.GetPath()} for {trainCar.ID}, is LocoResourceReceiver component."); + //} + } + + return false; + } +} diff --git a/Multiplayer/Patches/Train/TrainCarPatch.cs b/Multiplayer/Patches/Train/TrainCarPatch.cs index 5b92947b..4f807633 100644 --- a/Multiplayer/Patches/Train/TrainCarPatch.cs +++ b/Multiplayer/Patches/Train/TrainCarPatch.cs @@ -41,4 +41,11 @@ private static void Rerail_Prefix(TrainCar __instance, RailTrack rerailTrack, Ve return; NetworkLifecycle.Instance.Server.SendRerailTrainCar(networkedTrainCar.NetId, NetworkedRailTrack.GetFromRailTrack(rerailTrack).NetId, worldPos - WorldMover.currentMove, forward); } + + [HarmonyPrefix] + [HarmonyPatch(nameof(TrainCar.UpdateCouplerJoints))] + private static bool UpdateCouplerJoints(TrainCar __instance) + { + return NetworkLifecycle.Instance.IsHost(); + } } diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs new file mode 100644 index 00000000..d37f704e --- /dev/null +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -0,0 +1,48 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Text; +using DV.Logic.Job; +using DV.Optimizers; + +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(() => + { + 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 = TrainCarRegistry.Instance.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 00000000..a85f911d --- /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 00000000..8fec5588 --- /dev/null +++ b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs @@ -0,0 +1,104 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Reflection.Emit; +using UnityEngine; + + +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 40949f22..3f32e50a 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 00000000..fea9544c --- /dev/null +++ b/Multiplayer/Patches/Util/DVSteamworksPatch.cs @@ -0,0 +1,18 @@ +using DV.Platform.Steam; +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 00000000..afc0c749 --- /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/CashRegisterBasePatch.cs b/Multiplayer/Patches/World/CashRegisterBasePatch.cs new file mode 100644 index 00000000..8ffead33 --- /dev/null +++ b/Multiplayer/Patches/World/CashRegisterBasePatch.cs @@ -0,0 +1,115 @@ +using DV.CashRegister; +using DV.InventorySystem; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(CashRegisterBase))] +public class CashRegisterBasePatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.AddCash))] + private static bool AddCash(CashRegisterBase __instance, double amount) + { + if (__instance is not CashRegisterWithModules cashRegisterWithModules) + return true; + + Multiplayer.LogDebug(() => $"AddCash() {__instance.GetObjectPath()}, Deposited: {amount}\r\n{Environment.StackTrace}"); + + if (!NetworkedCashRegisterWithModules.TryGet(cashRegisterWithModules, out var netCashRegister)) + { + Multiplayer.LogWarning($"Attempting to AddCash, but NetworkedCashRegisterWithModules not found for {cashRegisterWithModules.GetObjectPath()}"); + return true; + } + + if (netCashRegister.IsShopRegister) + return true; + + Inventory.Instance.AddMoney(amount); + + CoroutineManager.Instance.StartCoroutine(netCashRegister.AddCash(amount)); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.OnEnable))] + private static bool OnEnable(CashRegisterBase __instance) + { + //Multiplayer.LogDebug(() => $"CashRegisterBase.OnEnable({__instance.GetObjectPath()}) {__instance.GetType()}"); + if (__instance is not CashRegisterWithModules) + return true; + + return NetworkLifecycle.Instance.IsHost(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterBase.OnDisable))] + private static bool OnDisable(CashRegisterBase __instance) + { + //Multiplayer.LogDebug(() => $"CashRegisterBase.OnDisable({__instance.GetObjectPath()}) {__instance.GetType()}"); + if (__instance is not CashRegisterWithModules) + return true; + + // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area + __instance.StopAllCoroutines(); + return NetworkLifecycle.Instance.IsHost(); + } +} + +[HarmonyPatch] +public class CashRegisterBaseReturnMoneyToPlayerCheckPatch +{ + const int TARGET_NOPS = 3; + static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); + + public static IEnumerable TargetMethods() + { + //We're targeting an 'IEnumerable'; this gets compiled as a state machine with + //a method per state. + //Find all of the resultant states that are a 'MoveNext', these are the methods we need to patch. + //Doing this dynamically reduces the chance a game update breaks the transpiler + return typeof(CashRegisterBase) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.Name == "MoveNext"); + } + + private static IEnumerable Transpiler(IEnumerable instructions) + { + int nopCtr = 0; + bool foundEntry = false; + + List newCode = [] ; + + foreach (CodeInstruction instruction in instructions) + { + if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) + { + foundEntry = true; + newCode.Add(CodeInstruction.Call(typeof(DvExtensions), nameof(DvExtensions.AnyPlayerSqrMag), [typeof(Vector3)], null)); //inject our method + } + else if (foundEntry && nopCtr < TARGET_NOPS) + { + nopCtr++; + newCode.Add(new CodeInstruction(OpCodes.Nop)); + } + else + { + newCode.Add(instruction); + } + } + + return newCode; + } +} diff --git a/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs new file mode 100644 index 00000000..777ed144 --- /dev/null +++ b/Multiplayer/Patches/World/CashRegisterWithModulesPatch.cs @@ -0,0 +1,123 @@ +using DV.CashRegister; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Packets.Common; +using Multiplayer.Utils; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(CashRegisterWithModules))] +public class CashRegisterWithModulesPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterWithModules.OnDisable))] + private static bool OnDisable(CashRegisterWithModules __instance) + { + //Multiplayer.LogDebug(() => $"CashRegisterWithModules.OnDisable({__instance.GetObjectPath()})"); + + __instance.StopAllCoroutines(); + __instance.textController.Clear(); + __instance.SetupListeners(false); + + // Prevent clients from cancelling/returning cash on cash registers when loading the game or leaving the area + return NetworkLifecycle.Instance.IsHost(); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterWithModules.OnBuyPressed))] + private static bool OnBuyPressed(CashRegisterWithModules __instance) + { + var player = PlayerManager.PlayerTransform.position; + var reg = __instance.transform.position; + var sqrMag = (player - reg).sqrMagnitude; + Multiplayer.LogDebug(() => $"CashRegisterWithModules.OnBuyPressed() player pos: {player} register pos: {reg}, sqrMag: {sqrMag}"); + if (NetworkLifecycle.Instance.IsHost()) + return true; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.OnBuyPressed({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return false; + } + + if (netCashRegister.IsShopRegister) + return true; + + CoroutineManager.Instance.StartCoroutine(netCashRegister.Buy()); + + return false; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterWithModules.OnBuyPressed))] + private static void OnBuyPressed_Postfix(CashRegisterWithModules __instance) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.OnBuyPressed_Postfix({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return; + } + + // Send buy action to all clients + NetworkLifecycle.Instance.Server.SendCashRegisterAction(new CommonCashRegisterWithModulesActionPacket + { + NetId = netCashRegister.NetId, + Action = CashRegisterAction.Buy, + Amount = __instance.DepositedCash + }); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] + private static bool Cancel(CashRegisterWithModules __instance) + { + + //Multiplayer.LogDebug(()=>$"CashRegisterWithModules.Cancel({__instance.GetObjectPath()})\r\n{Environment.StackTrace}"); + + if (NetworkLifecycle.Instance.IsHost()) + return true; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.Cancel({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return false; + } + + if (netCashRegister.IsShopRegister) + return true; + + CoroutineManager.Instance.StartCoroutine(netCashRegister.Cancel()); + + return false; + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(CashRegisterWithModules.Cancel))] + private static void Cancel_Postfix(CashRegisterWithModules __instance) + { + //Multiplayer.LogWarning($"CashRegisterWithModules.Cancel_Postfix({__instance.GetObjectPath()})"); + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedCashRegisterWithModules.TryGet(__instance, out var netCashRegister)) + { + Multiplayer.LogWarning($"CashRegisterWithModules.Cancel_Postfix({__instance.GetObjectPath()}) NetworkedCashRegisterWithModules not found!"); + return; + } + + if (netCashRegister.IsShopRegister) + return; + + // Send cancel action to all clients + NetworkLifecycle.Instance.Server.SendCashRegisterAction(new CommonCashRegisterWithModulesActionPacket + { + NetId = netCashRegister.NetId, + Action = CashRegisterAction.Cancel, + Amount = __instance.DepositedCash + }); + } +} diff --git a/Multiplayer/Patches/World/GenericSwitchPatch.cs b/Multiplayer/Patches/World/GenericSwitchPatch.cs new file mode 100644 index 00000000..4369f48f --- /dev/null +++ b/Multiplayer/Patches/World/GenericSwitchPatch.cs @@ -0,0 +1,31 @@ +using DV.Interaction; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using System.Collections; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(GenericSwitch))] +public class GenericSwitchPatch +{ + [HarmonyPatch(typeof(GenericSwitch), MethodType.Constructor)] + [HarmonyPostfix] + + public static void GenericSwitch_Constructor(GenericSwitch __instance) + { + Multiplayer.LogDebug(() => $"GenericSwitch.Constructor() persistenceKey: {__instance.persistenceKey}"); + CoroutineManager.Instance.StartCoroutine(WaitForGenericSwitch(__instance)); + } + + private static IEnumerator WaitForGenericSwitch(GenericSwitch genericSwitch) + { + + while (string.IsNullOrEmpty(genericSwitch.persistenceKey)) + yield return new WaitForEndOfFrame(); + + Multiplayer.LogDebug(() => $"WaitForGenericSwitch() persistenceKey: {genericSwitch.persistenceKey}"); + + genericSwitch.gameObject.AddComponent(); + } +} diff --git a/Multiplayer/Patches/World/Items/FlashlightPatch.cs b/Multiplayer/Patches/World/Items/FlashlightPatch.cs new file mode 100644 index 00000000..3a5f7131 --- /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 00000000..333e5c74 --- /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 00000000..9c899ec4 --- /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 00000000..01f97641 --- /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 00000000..ba23a468 --- /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 00000000..d62c9242 --- /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 00000000..737c6c15 --- /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 00000000..5234c993 --- /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/PlayerDistanceGameObjectsDisablerPatch.cs b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs new file mode 100644 index 00000000..ade5de6d --- /dev/null +++ b/Multiplayer/Patches/World/PlayerDistanceGameObjectsDisablerPatch.cs @@ -0,0 +1,153 @@ +using DV.Optimizers; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch] +public static class PlayerDistanceGameObjectsDisablerPatch +{ + const int SKIPS = 2; + static readonly CodeInstruction targetMethod = CodeInstruction.Call(typeof(Vector3), "op_Subtraction", [typeof(Vector3), typeof(Vector3)], null); + static readonly CodeInstruction newMethod = CodeInstruction.Call(typeof(PlayerDistanceGameObjectsDisablerPatch), nameof(CustomCalcSqrMagnitude), [typeof(Vector3), typeof(Vector3), typeof(PlayerDistanceGameObjectsDisabler)], null); + + + public static IEnumerable TargetMethods() + { + //We're targeting an 'IEnumerable'; this gets compiled as a state machine with + //a method per state. + //Find all of the resultant states that are a 'MoveNext', these are the methods we need to patch. + //Doing this dynamically reduces the chance a game update breaks the transpiler + return typeof(PlayerDistanceGameObjectsDisabler) + .GetNestedTypes(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t => t.Name.StartsWith("")) + .SelectMany(t => t.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)) + .Where(m => m.Name == "MoveNext"); + } + + + /* + * We want to find the call to Vector3 subtraction `(optimizingGameObjects[i].transform.position - position)` + * (found on line 79 of the IL code) and replace it with an instruction + * that loads the current instance "this" to the stack. + * we want to override line 80 so it calls our custom method `CustomCalcSqrMagnitude()` + * Lines 81 and 82 are not required and need to be NOP'd out + * This pattern is used again in the re-enable check (lines 104 - 115) + + 74 00D6 ldfld int32 PlayerDistanceGameObjectsDisabler/'d__6'::'5__2' + 75 00DB callvirt instance !0 class [mscorlib] System.Collections.Generic.List`1::get_Item(int32) + 76 00E0 callvirt instance class [UnityEngine.CoreModule] + UnityEngine.Transform[UnityEngine.CoreModule] UnityEngine.GameObject::get_transform() + 77 00E5 callvirt instance valuetype[UnityEngine.CoreModule] UnityEngine.Vector3 [UnityEngine.CoreModule] UnityEngine.Transform::get_position() + 78 00EA ldloc.2 //parameter for the position of the player's camera + + //overwrite line 79 with ldloc.1 (pass in 'this' as the final parameter of call to CustomCalcSqrMagnitude()) + 79 00EB call valuetype[UnityEngine.CoreModule] UnityEngine.Vector3[UnityEngine.CoreModule] UnityEngine.Vector3::op_Subtraction(valuetype[UnityEngine.CoreModule] UnityEngine.Vector3, valuetype[UnityEngine.CoreModule] UnityEngine.Vector3) + //overwrite with call to CustomCalcSqrMagnitude() (techinically we are inserting the call and skipping the original) + //Insert 3 NOPs + 80 00F0 stloc.3 //skip 0 + 81 00F1 ldloca.s V_3(3) //skip 1 + 82 00F3 call instance float32[UnityEngine.CoreModule] UnityEngine.Vector3::get_sqrMagnitude() //Skip 2 + 83 00F8 ldloc.1 + 84 00F9 ldfld float32 PlayerDistanceGameObjectsDisabler::disableSqrDistance + 85 00FE ble.un.s 94 (0119) ldloc.1 + + */ + [HarmonyTranspiler] + public static IEnumerable GameObjectsDistanceCheck(IEnumerable instructions) + { + //Multiplayer.LogDebug(() => + //{ + // var code = new List(instructions); + + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("Starting transpiler"); + // sb.AppendLine("IL Before:"); + // for (int i = 0; i < code.Count; i++) + // sb.AppendLine($"{i:D4}: {code[i]}"); + + // return sb.ToString(); + //}); + + int skipCtr = 0; + bool skipFlag = false; + + var newCode = new List(); + + foreach (CodeInstruction instruction in instructions) + { + //Multiplayer.LogDebug(() => $"Checking instruction: {instruction}"); + if (instruction.opcode == OpCodes.Call && instruction.operand?.ToString() == targetMethod.operand?.ToString()) + { + //Multiplayer.LogDebug(() => "Found target method, replacing"); + newCode.Add(new CodeInstruction(OpCodes.Ldloc_1)); + newCode.Add(newMethod); //skip 0 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 1 + newCode.Add(new CodeInstruction(OpCodes.Nop)); //skip 2 + skipCtr = 0; //reset as there are 2 identical sections to the code to be patched. + skipFlag = true; + } + else if (skipFlag) + { + if (skipCtr == SKIPS) + { + skipFlag = false; //stop skipping + continue; + } + skipCtr++; + } + else + newCode.Add(instruction); + } + + //Multiplayer.LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("IL After:"); + // for (int i = 0; i < newCode.Count; i++) + // sb.AppendLine($"{i:D4}: {newCode[i]}"); + + // return sb.ToString(); + //}); + + return newCode; + } + + public static float CustomCalcSqrMagnitude(Vector3 vecA, Vector3 vecB, PlayerDistanceGameObjectsDisabler instance) + { + //Ensure we are only using the custom calc for certain instances and we are the host + if (ShouldUseCustomCalc(instance) && NetworkLifecycle.Instance.IsHost()) + { + //Multiplayer.LogDebug(() =>$"CustomCalcSqrMagnitude({instance?.gameObject?.name}, {vecA}, {vecB}) Camera pos: {PlayerManager.ActiveCamera.transform.position}"); + return vecA.AnyPlayerSqrMag(); + } + + return (vecA - vecB).sqrMagnitude; + } + + private static bool ShouldUseCustomCalc(PlayerDistanceGameObjectsDisabler instance) + { + var go = instance.gameObject; + + //At present we only need to target certain instances of `PlayerDistanceGameObjectsDisabler` + //we need these to be active on the host when any player is nearby. + + //Ensure refill stations are enabled + if (go.name == "RefillStations") + return true; + + //Ensure warehouse machines are enabled + var parent = go.transform.parent; + if (parent != null && parent.name.EndsWith("_office_anchor")) + return true; + + //Ignore all other instances + return false; + } +} diff --git a/Multiplayer/Patches/World/PluggableObjectPatch.cs b/Multiplayer/Patches/World/PluggableObjectPatch.cs new file mode 100644 index 00000000..f2ab7fea --- /dev/null +++ b/Multiplayer/Patches/World/PluggableObjectPatch.cs @@ -0,0 +1,59 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(PluggableObject))] +public static class PluggableObjectPatch +{ + [HarmonyPatch(nameof(PluggableObject.Awake))] + [HarmonyPrefix] + public static bool Awake(PluggableObject __instance) + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + // Allow the client to setup the plug, but don't allow `InstantSnapTo(this.startAttachedTo);` to be called + __instance.CheckInitialization(); + return false; + } + + [HarmonyPatch(nameof(PluggableObject.IsHeldInHand), MethodType.Getter)] + [HarmonyPrefix] + public static bool IsHeldInHand(PluggableObject __instance, ref bool __result) + { + var result = __result; + //Multiplayer.LogDebug(() => $"IsHeldInHand({result})"); + + if (NetworkedPluggableObject.Get(__instance, out var networkedPluggableObject)) + __result = networkedPluggableObject.IsHeld; + else + __result = __instance.controlGrabbed; + + result = __result; + //Multiplayer.LogDebug(() => $"IsHeldInHand() result: {result}, net found: {networkedPluggableObject != null}"); + + return false; + } + + [HarmonyPatch(nameof(PluggableObject.ConnectingRoutine))] + [HarmonyPrefix] + public static void ConnectingRoutine(PluggableObject __instance) + { + Multiplayer.LogDebug(() => $"ConnectingRoutine()"); + if (NetworkedPluggableObject.Get(__instance, out var networkedPluggableObject)) + { + networkedPluggableObject.IsConnecting = true; + } + } + + //[HarmonyPatch(typeof(PluggableObject), nameof(PluggableObject.InstantSnapTo))] + //[HarmonyPrefix] + //public static void InstantSnapTo_Prefix(PluggableObject __instance, PlugSocket socket) + //{ + // Multiplayer.LogDebug(() => $"PluggableObject.InstantSnapTo() called: {__instance.GetObjectPath()} -> {socket.GetObjectPath()}\r\n {Environment.StackTrace}"); + //} +} diff --git a/Multiplayer/Patches/World/PropHosePatch.cs b/Multiplayer/Patches/World/PropHosePatch.cs new file mode 100644 index 00000000..8ee22e33 --- /dev/null +++ b/Multiplayer/Patches/World/PropHosePatch.cs @@ -0,0 +1,212 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System.Collections.Generic; +using System.Reflection; +using System.Reflection.Emit; +using UnityEngine; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(PropHose))] +public static class PropHosePatch +{ + static readonly CodeInstruction targetUnplugMethod = CodeInstruction.Call(typeof(PluggableObject), nameof(PluggableObject.Unplug), [], null); + static readonly CodeInstruction override_UnplugMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_Unplug), [typeof(PluggableObject)], null); + + static readonly CodeInstruction targetYankOutOfHandMethod = CodeInstruction.Call(typeof(PluggableObject), nameof(PluggableObject.YankOutOfHand), [], null); + static readonly CodeInstruction override_YankOutOfHandMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_YankOutOfHand), [typeof(PluggableObject)], null); + + static readonly CodeInstruction targetAddForceMethod = CodeInstruction.Call(typeof(Rigidbody), nameof(Rigidbody.AddForce), [typeof(Vector3), typeof(ForceMode)], null); + static readonly CodeInstruction override_AddForceMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_AddForce), [typeof(Rigidbody), typeof(Vector3), typeof(ForceMode), typeof(PluggableObject)], null); + + static readonly CodeInstruction targetInstantSnapToMethod = CodeInstruction.Call(typeof(PluggableObject), nameof(PluggableObject.InstantSnapTo), [typeof(PlugSocket)], null); + static readonly CodeInstruction override_InstantSnapToMethod = CodeInstruction.Call(typeof(PropHosePatch), nameof(PropHosePatch.Override_InstantSnapTo), [typeof(PluggableObject), typeof(PlugSocket)], null); + + private static readonly string UnplugOperand = targetUnplugMethod.operand?.ToString(); + private static readonly string YankOutOfHandOperand = targetYankOutOfHandMethod.operand?.ToString(); + private static readonly string AddForceOperand = targetAddForceMethod.operand?.ToString(); + private static readonly string InstantSnapToOperand = targetInstantSnapToMethod.operand?.ToString(); + + [HarmonyPatch(nameof(PropHose.OnEnable))] + [HarmonyPrefix] + public static bool OnEnable() + { + if (NetworkLifecycle.Instance.IsHost()) + return true; + + //prevent client from snapping manual service plug to home position + return false; + } + + [HarmonyPatch(nameof(PropHose.Update))] + [HarmonyTranspiler] + public static IEnumerable Update(IEnumerable instructions) + { + var newCode = new List(); + + //Multiplayer.LogDebug(() => + //{ + // var code = new List(instructions); + + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("Starting transpiler PropHose.Update"); + // sb.AppendLine("IL Before:"); + // for (int i = 0; i < code.Count; i++) + // sb.AppendLine($"{i:D4}: {code[i]}"); + // return sb.ToString(); + //}); + + + foreach (CodeInstruction instruction in instructions) + { + + if (instruction.opcode == OpCodes.Callvirt) + { + string operand = instruction.operand?.ToString(); + + if (operand == UnplugOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to PluggableObject on the stack as `this.plug` (first param of CallVirt) + //the next item on the stack is the PlugSocket + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_UnplugMethod}"); + + //call our override method + newCode.Add(override_UnplugMethod); + + } + else if (operand == YankOutOfHandOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to PluggableObject on the stack as `this.plug` (first param of CallVirt) + //the next item on the stack is the PlugSocket + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_YankOutOfHandMethod}"); + + //call our override method + newCode.Add(override_YankOutOfHandMethod); + } + else if (operand == AddForceOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to rb on the stack as `this.plugBody` (first param of CallVirt) + //the next item on the stack is the force, then the mode + //we will manually add the plug instance + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_AddForceMethod}"); + + //load instance/"this" to the stack + newCode.Add(new CodeInstruction(OpCodes.Ldarg_0)); + //load PropHose.plug reference on to the stack ("this.plug") + newCode.Add(new CodeInstruction(OpCodes.Ldfld, typeof(PropHose).GetField(nameof(PropHose.plug), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))); + //call our override method + newCode.Add(override_AddForceMethod); + + } + else if (operand == InstantSnapToOperand) + { + //We are switching from a 'CallVirt' to a 'Call'. + //we already have a reference to PluggableObject on the stack as `this.plug` (first param of CallVirt) + //the next item on the stack is the PlugSocket + + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}, replacing: {override_InstantSnapToMethod}"); + + //call our override method + newCode.Add(override_InstantSnapToMethod); + } + else + { + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}"); + newCode.Add(instruction); + } + } + else + { + //Multiplayer.LogDebug(() => $"PropHose.Update() {instruction}"); + newCode.Add(instruction); + } + } + + //Multiplayer.LogDebug(() => + //{ + // StringBuilder sb = new StringBuilder(); + // sb.AppendLine("IL After:"); + // for (int i = 0; i < newCode.Count; i++) + // sb.AppendLine($"{i:D4}: {newCode[i]}"); + + // return sb.ToString(); + //}); + + return newCode; + } + + private static void Override_Unplug(PluggableObject instance) + { + Multiplayer.LogDebug(() => $"Override_Unplug({instance.GetObjectPath()})"); + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + Multiplayer.LogDebug(() => $"Override_Unplug({instance.GetObjectPath()}) Unplugging"); + instance.Unplug(); + } + + private static bool Override_YankOutOfHand(PluggableObject instance) + { + Multiplayer.LogDebug(() => $"Override_YankOutOfHand({instance.GetObjectPath()})"); + + if (!NetworkLifecycle.Instance.IsHost()) + return false; // result is unused by Update(), we can return true or false + + if (NetworkedPluggableObject.Get(instance, out var netPlug)) + netPlug.DropPlug(); + + Multiplayer.LogDebug(() => $"Override_YankOutOfHand({instance.GetObjectPath()}) Yanking"); + return instance.YankOutOfHand(); + } + + private static void Override_AddForce(Rigidbody rb, Vector3 force, ForceMode mode, PluggableObject instance) + { + Multiplayer.LogDebug(() => $"Override_AddForce() station: {instance.GetObjectPath()}, force: {force}, mode: {mode}"); + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (NetworkedPluggableObject.Get(instance, out var netPlug)) + { + Multiplayer.LogDebug(() => $"Override_AddForce() station: {netPlug.Station.StationName}, force: {force}, mode: {mode}"); + + //The force will be applied when the packet is processed as the host + //rb.AddForce(force, mode); + netPlug.YankedByRope(force, mode); + } + } + + private static bool Override_InstantSnapTo(PluggableObject instance, PlugSocket socket) + { + Multiplayer.LogDebug(() => $"Override_InstantSnapTo({instance.GetObjectPath()}, {socket.GetObjectPath()}) instance.yankOutOfHand: {instance.yankOutOfHand}"); + + if (!NetworkLifecycle.Instance.IsHost()) + return false; // result is unused by Update(), we can return true or false + + if(!instance.yankOutOfHand) + { + Multiplayer.LogDebug(() => $"Override_InstantSnapTo({instance.GetObjectPath()}, {socket.GetObjectPath()}) Blocked by yank settlement"); + return false; + } + + Multiplayer.LogDebug(() => $"Override_InstantSnapTo({instance.GetObjectPath()}, {socket.GetObjectPath()}) Snapping"); + + if (NetworkedPluggableObject.Get(instance, out var netPlug)) + { + netPlug.SnappedByRope(); + return false; + } + + // no player holding, we can allow the snap + return instance.InstantSnapTo(socket); + } +} diff --git a/Multiplayer/Patches/World/SaveGameManagerPatch.cs b/Multiplayer/Patches/World/SaveGameManagerPatch.cs index 0c8067ff..c014da70 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 3906a85f..b36b4a9f 100644 --- a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs +++ b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs @@ -1,11 +1,12 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using DV.Logic.Job; using DV.ThingTypes; using DV.Utils; using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data; +using Multiplayer.Utils; using UnityEngine; namespace Multiplayer.Patches.World; @@ -22,7 +23,7 @@ private static void Postfix(StationLocoSpawner __instance) private static IEnumerator WaitForSetup(StationLocoSpawner __instance) { - if (!AStartGameData.carsAndJobsLoadingFinished || SingletonBehaviour.Instance.PoolSetupInProgress) + if (!AStartGameData.carsAndJobsLoadingFinished || CarSpawner.Instance.PoolSetupInProgress) yield return null; while (NetworkLifecycle.Instance.Client == null) yield return null; @@ -37,7 +38,7 @@ 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,25 +53,19 @@ 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(); + List carsFullyOnTrack = stationLocoSpawner.locoSpawnTrack.LogicTrack().GetCarsFullyOnTrack(); if (carsFullyOnTrack.Count != 0 && carsFullyOnTrack.Exists(car => CarTypes.IsLocomotive(car.carType))) return; List trainCarTypes = new(stationLocoSpawner.locoTypeGroupsToSpawn[stationLocoSpawner.nextLocoGroupSpawnIndex].liveries); stationLocoSpawner.nextLocoGroupSpawnIndex = Random.Range(0, stationLocoSpawner.locoTypeGroupsToSpawn.Count); - List unusedTrainCars = - SingletonBehaviour.Instance.SpawnCarTypesOnTrack(trainCarTypes, null, stationLocoSpawner.locoSpawnTrack, true, true, flipTrainConsist: stationLocoSpawner.spawnRotationFlipped); + List unusedTrainCars = + CarSpawner.Instance.SpawnCarTypesOnTrack(trainCarTypes, null, stationLocoSpawner.locoSpawnTrack, true, true, flipTrainConsist: stationLocoSpawner.spawnRotationFlipped) + .Select(TC => TC.logicCar).ToList(); + if (unusedTrainCars != null) - SingletonBehaviour.Instance.MarkForDelete(unusedTrainCars); + UnusedTrainCarDeleter.Instance.MarkForDelete(unusedTrainCars); } } diff --git a/Multiplayer/Patches/World/StorageControllerPatch.cs b/Multiplayer/Patches/World/StorageControllerPatch.cs new file mode 100644 index 00000000..df1e9813 --- /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 c01fe674..3e78fbfe 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,5 +1,7 @@ -using System; +using System; using Humanizer; +using Multiplayer.Components.MainMenu; +using Multiplayer.Utils; using UnityEngine; using UnityModManagerNet; using Console = DV.Console; @@ -10,24 +12,57 @@ namespace Multiplayer; [DrawFields(DrawFieldMask.OnlyDrawAttr)] public class Settings : UnityModManager.ModSettings, IDrawable { + public const int CURRENT_VERSION = 3; public const byte MAX_USERNAME_LENGTH = 24; public static Action OnSettingsUpdated; + public int SettingsVer = CURRENT_VERSION; + [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("Misc.")] + [Draw("Chat Key Bind", Tooltip ="Key to show chat window.")] + public KeyCode ChatKey = KeyCode.Return; + [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("Server Visibility")] + public ServerVisibility Visibility = ServerVisibility.Public; + 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"; + [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.")] @@ -41,7 +76,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable public bool ShowAdvancedSettings; [Draw("Show Stats", Tooltip = "Whether to show network statistics.", VisibleOn = "ShowAdvancedSettings|true")] public bool ShowStats; - [Draw("Stats List Size", Tooltip = "How many packets to list in the network statistics gui.", VisibleOn = "ShowStats|true")] + [Draw("Stats List Size", Tooltip = "How many packets to list in the network statistics GUI.", VisibleOn = "ShowStats|true")] public int StatsListSize = 3; [Draw("Debug Logging", Tooltip = "Whether to log extra information. This is useful for debugging, but should otherwise be kept off.", VisibleOn = "ShowAdvancedSettings|true")] public bool DebugLogging; @@ -65,7 +100,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,10 +111,15 @@ 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); Password = Password?.Trim(); + + ChatKey = ChatKey == KeyCode.None ? KeyCode.Return : ChatKey; + if (!UnloadWatcher.isQuitting) OnSettingsUpdated?.Invoke(this); Save(this, modEntry); @@ -98,4 +138,86 @@ 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 CURRENT_VERSION; + } + + // 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; + + //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; + + 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; + + case 2: + + if (data.PublicGame) + data.Visibility = ServerVisibility.Public; + else + data.Visibility = ServerVisibility.Friends; + + break; + + default: + break; + } + + if (data.SettingsVer < GetCurrentVersion()) + { + data.SettingsVer++; + MigrateSettings(ref data); + } + } } diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index 560fb24f..13943da6 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 745ef944..6dcacb16 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -1,6 +1,16 @@ -using System; +using DV.Interaction; +using DV.KeyboardInput; +using DV.Localization; +using DV.UI; +using DV.UIFramework; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using UnityEngine; +using UnityEngine.UI; + + 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,120 @@ 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.initialized = false; + 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 (sqDist < result) + result = sqDist; + } + + return result; + } + + public static bool PlayerCanReach(this GameObject item, ServerPlayer player, float extraRange = 0f) + { + return PlayerCanReach (item.transform, player, extraRange); + } + + public static bool PlayerCanReach(this Transform item, ServerPlayer player, float extraRange = 0f) + { + float reachRange = AKeyboardInput.XZ_SQR_REACH_RANGE + GrabberRaycasterDV.FPS_INTERACTION_RANGE_SQR + (extraRange * extraRange); + + var delta = player.WorldPosition - item.transform.position; + + if (Mathf.Abs(delta.y) > AKeyboardInput.Y_REACH_RANGE) + return false; + + delta.y = 0f; + + float sqrMag = (delta).sqrMagnitude; + + return sqrMag <= reachRange; + } + + 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 00000000..39baeefa --- /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 00000000..e4d2115c --- /dev/null +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -0,0 +1,231 @@ +using DV.Localization; +using DV.Platform.Steam; +using DV.UIFramework; +using DV; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; +using Steamworks.Data; +using Steamworks; +using System.Collections; +using System.Linq; +using System; +using UnityEngine; + +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() ?? ""; + if (prop.Name == nameof(LobbyServerData.RequiredMods)) + { + try + { + value = Newtonsoft.Json.JsonConvert.SerializeObject((ModInfo[])prop.GetValue(data)); + } + catch (Exception ex) + { + Multiplayer.LogException($"SetLobbyData() Error serializing RequiredMods property", ex); + } + + Multiplayer.LogDebug(() => $"SetLobbyData() Setting property: {prop.Name}, value: {value}"); + } + lobby.SetData(prop.Name, value); + } + } + + public static LobbyServerData GetLobbyData(this Lobby lobby) + { + var data = new LobbyServerData(); + var properties = typeof(LobbyServerData).GetProperties(); + string value = null; + + foreach (var prop in properties) + { + try + { + value = lobby.GetData(prop.Name); + if (string.IsNullOrEmpty(value)) continue; + + Multiplayer.LogDebug(() => $"GetLobbyData() Retrieving property: {prop.Name}, value: {value}"); + + // Backward compatibility for non-JSON strings + if (prop.Name == nameof(LobbyServerData.RequiredMods)) + { + var mods = ModInfo.DeserializeRequiredMods(value); + + prop.SetValue(data, mods); + continue; + } + + if (prop.PropertyType.IsEnum) + { + var enumValue = Enum.Parse(prop.PropertyType, value); + prop.SetValue(data, enumValue); + } + else + { + var converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(data, converted); + } + + value = null; + } + catch (Exception ex) + { + Multiplayer.LogException($"GetLobbyData() Error parsing property: {prop?.Name}, value: {value}", ex); + } + } + + 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 IEnumerator JoinFromCommandLine() + { + float time = Time.time; + + Multiplayer.LogDebug(() => $"JoinFromCommandLine() {DVSteamworks.Success}"); + + if (hasJoinedCL || BuildInfo.BUILD_DESTINATION != "steam") + yield break; + + hasJoinedCL = true; + + //allow steamworks to initialise + yield return new WaitUntil(() => { return DVSteamworks.Success || (Time.deltaTime - time) > 5; }); + + if (!DVSteamworks.Success) + yield break; + + var id = GetLobbyIdFromArgs(); + var sId = new SteamId + { + Value = id + }; + + var lobby = new Lobby(sId); + lobby.Refresh(); + + QueueLobbyInvite(lobby); + } + + 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; + + lobby.Refresh(); + + QueueLobbyInvite(lobby); + } + + public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) + { + Multiplayer.Log($"Received lobby invite from '{friend.Name}' ({friend.Id}), Lobby: {lobby.Id}"); + + if (!CanHandleLobbyRequest()) + return; + + lobby.Refresh(); + + 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) => + { + Multiplayer.LogDebug(() => $"Agreed to join: {result.closedBy}"); + 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/StringHashing.cs b/Multiplayer/Utils/StringHashing.cs new file mode 100644 index 00000000..4d0a7b57 --- /dev/null +++ b/Multiplayer/Utils/StringHashing.cs @@ -0,0 +1,19 @@ +namespace Multiplayer.Utils; + +internal static class StringHashing +{ + public static uint Fnv1aHash(string text) + { + unchecked + { + const uint fnvPrime = 0x01000193; + uint hash = 0x811C9DC5; + foreach (char c in text) + { + hash ^= c; + hash *= fnvPrime; + } + return hash; + } + } +} diff --git a/Multiplayer/Utils/UnityExtensions.cs b/Multiplayer/Utils/UnityExtensions.cs index ed75e18a..e91c770b 100644 --- a/Multiplayer/Utils/UnityExtensions.cs +++ b/Multiplayer/Utils/UnityExtensions.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Text; using JetBrains.Annotations; using UnityEngine; @@ -105,4 +106,35 @@ 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); + } + + public static string GetObjectPath(this Component component) + { + return component.gameObject.GetObjectPath(); + } + + public static string GetObjectPath(this GameObject obj) + { + if (obj.transform.parent == null) + return obj.name; + + return obj.transform.parent.gameObject.GetObjectPath() + "/" + obj.name; + } } diff --git a/MultiplayerAPI Tests/Enums/WheelArrangement.cs b/MultiplayerAPI Tests/Enums/WheelArrangement.cs new file mode 100644 index 00000000..c887fe1d --- /dev/null +++ b/MultiplayerAPI Tests/Enums/WheelArrangement.cs @@ -0,0 +1,10 @@ +namespace MultiplayerAPITest.Enums; + +//for dynamic info a hash or other numbering system could be used, rather than a static enum. +public enum WheelArrangement : byte +{ + Default = 0, + American440 = 1, + Atlantic442 = 2, + Reading444 = 3 +} diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.cs b/MultiplayerAPI Tests/MultiplayerAPITest.cs new file mode 100644 index 00000000..489962bc --- /dev/null +++ b/MultiplayerAPI Tests/MultiplayerAPITest.cs @@ -0,0 +1,126 @@ +using HarmonyLib; +using JetBrains.Annotations; +using MPAPI; +using MPAPI.Interfaces; +using MPAPI.Types; +using MultiplayerAPITest.TestComponents; +using System; +using UnityEngine; +using UnityModManagerNet; + +namespace MultiplayerAPITest; + +public static class MultiplayerAPITest +{ + public static UnityModManager.ModEntry ModEntry; + + [UsedImplicitly] + public static bool Load(UnityModManager.ModEntry modEntry) + { + ModEntry = modEntry; + //Settings = Settings.Load(modEntry); + //ModEntry.OnGUI = Settings.Draw; + //ModEntry.OnSaveGUI = Settings.Save; + ModEntry.OnLateUpdate = LateUpdate; + + Harmony harmony = null; + + try + { + Log($"Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}"); + + Log("Patching..."); + harmony = new Harmony(ModEntry.Info.Id); + harmony.PatchAll(); + + Log("Loaded!"); + } + catch (Exception ex) + { + LogException("Failed to load:", ex); + harmony?.UnpatchAll(); + return false; + } + + return true; + } + + + private static void LateUpdate(UnityModManager.ModEntry modEntry, float dt) + { + //Mod loading order can't be guaranteed, so we should wait for all mods to load prior to checking for multiplayer. + //Alternatively, set 'Multiplayer' in the 'LoadAfter' parameter in your 'info.json' + Log($"MultiplayerAPITest.LateUpdate() Multiplayer Mod is loaded: {MultiplayerAPI.IsMultiplayerLoaded}, API Version: {MultiplayerAPI.LoadedApiVersion} "); + if (MultiplayerAPI.IsMultiplayerLoaded) + { + //Register that this mod needs to be installed on both server and client + MultiplayerAPI.Instance.SetModCompatibility(ModEntry.Info.Id, MultiplayerCompatibility.All); + + // Register for server start and client start events + // Note: for a non dedicated server both server and client events will be fired + MultiplayerAPI.ServerStarted += OnServerStarted; + MultiplayerAPI.ClientStarted += OnClientStarted; + } + + modEntry.OnLateUpdate -= LateUpdate; + } + + private static void OnServerStarted(IServer server) + { + // How you handle the server starting is up to you + // In this test/example mod we are injecting a server manager into the scene, but you could + // also just integrate it into your mod's existing workflow. + // Keep in mind on a non-dedicated server, both the client and server will run concurrently + GameObject go = new GameObject("MPAPI ServerTest", [typeof(ServerTest)]); + GameObject.DontDestroyOnLoad(go); + } + + private static void OnClientStarted(IClient client) + { + // How you handle the client starting is up to you + // In this test/example mod we are injecting a client manager into the scene, but you could + // also just integrate it into your mod's existing workflow. + // Keep in mind on a non-dedicated host, both the client and server will run concurrently + GameObject go = new GameObject("MPAPI ClientTest", [typeof(ClientTest)]); + GameObject.DontDestroyOnLoad(go); + } + + #region Logging + + public static void LogDebug(Func resolver) + { + //if (!Settings.DebugLogging) + // return; + WriteLog($"[Debug] {resolver.Invoke()}"); + } + + public static void Log(object msg) + { + WriteLog($"[Info] {msg}"); + } + + public static void LogWarning(object msg) + { + WriteLog($"[Warning] {msg}"); + } + + public static void LogError(object msg) + { + WriteLog($"[Error] {msg}"); + } + + public static void LogException(object msg, Exception e) + { + ModEntry.Logger.LogException($"{msg}", e); + } + + private static void WriteLog(string msg) + { + string str = $"[{DateTime.Now.ToUniversalTime():HH:mm:ss.fff}] {msg}"; + //if (Settings.EnableLogFile) + // File.AppendAllLines(LOG_FILE, new[] { str }); + ModEntry.Logger.Log(str); + } + + #endregion +} diff --git a/MultiplayerAPI Tests/MultiplayerAPITest.csproj b/MultiplayerAPI Tests/MultiplayerAPITest.csproj new file mode 100644 index 00000000..797b363f --- /dev/null +++ b/MultiplayerAPI Tests/MultiplayerAPITest.csproj @@ -0,0 +1,100 @@ + + + + net48 + latest + MultiplayerAPITest + 0.0.0.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../build/MultiplayerAPI.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MultiplayerAPI Tests/Packets/ComplexModPacket.cs b/MultiplayerAPI Tests/Packets/ComplexModPacket.cs new file mode 100644 index 00000000..be7a7e59 --- /dev/null +++ b/MultiplayerAPI Tests/Packets/ComplexModPacket.cs @@ -0,0 +1,49 @@ +using MPAPI.Interfaces.Packets; +using System.Collections.Generic; +using System.IO; +using UnityEngine; + +namespace MultiplayerAPITest.Packets +{ + internal class ComplexModPacket : ISerializablePacket + { + //Complex packets require manual serialization + //Altenatively, implement methods to convert complex data structures to/from arrays and use the automatic serialization + + public Dictionary CarToPositionMap { get; set; } + + public void Deserialize(BinaryReader reader) + { + //retrieve the dictionary length + var length = reader.ReadInt32(); + + CarToPositionMap = []; + + //retrieve each key and value + for (int i = 0; i < length; i++) + { + var key = reader.ReadString(); + var x = reader.ReadSingle(); + var y = reader.ReadSingle(); + var z = reader.ReadSingle(); + + CarToPositionMap.Add(key, new Vector3 (x, y, z)); + } + } + + public void Serialize(BinaryWriter writer) + { + //write out the length of the dictionary + writer.Write(CarToPositionMap.Count); + + //write out each key and value + foreach (var kvp in CarToPositionMap) + { + writer.Write(kvp.Key); + writer.Write(kvp.Value.x); + writer.Write(kvp.Value.y); + writer.Write(kvp.Value.z); + } + } + } +} diff --git a/MultiplayerAPI Tests/Packets/SimplePacket.cs b/MultiplayerAPI Tests/Packets/SimplePacket.cs new file mode 100644 index 00000000..07a247fa --- /dev/null +++ b/MultiplayerAPI Tests/Packets/SimplePacket.cs @@ -0,0 +1,26 @@ +using MPAPI.Interfaces.Packets; +using MultiplayerAPITest.Enums; +using UnityEngine; + +namespace MultiplayerAPITest.Packets +{ + internal class SimplePacket : IPacket + { + //Public properties are automatically serialised + //acceptable types are: + // Primitives (bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, char, IPEndPoint, Guid) + // Arrays of primitives + // Enums derived from primitives e.g. `enum MyEnum : byte` + // UnityEngine: Vector2, Vector3, Quaternion + + //Be mindful of the amount of data per packet. + // Avoid sending long strings or large structures + // Consider using a numeric Id system to represent objects. + // The MP API provides Net Ids for common objects (e.g. TrainCars, Jobs, Switches, Turntables and RailTrack), + // see `TryGetNetId(T obj, out ushort netId)` + public string CarId { get; set; } //It's better to use ushort. See SimplePacketWithNetId for an example + public Vector3 Position { get; set; } + public WheelArrangement WheelArrangement { get; set; } + + } +} diff --git a/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs b/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs new file mode 100644 index 00000000..18bcc9d8 --- /dev/null +++ b/MultiplayerAPI Tests/Packets/SimplePacketWithNetId.cs @@ -0,0 +1,26 @@ +using MPAPI.Interfaces.Packets; +using MultiplayerAPITest.Enums; +using UnityEngine; + + +namespace MultiplayerAPITest.Packets +{ + //Public properties are automatically serialised + //acceptable types are: + // Primitives and inbuilt structs (bool, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, string, char, IPEndPoint, Guid) + // Arrays of primitives (e.g bool[], byte[], etc. + // Enums derived from primitives e.g. `enum MyEnum : byte` + // UnityEngine: Vector2, Vector3, Quarternion + + //Be mindful of the amount of data per packet. + // Avoid sending long strings or large structures + // Consider using a numeric Id system to identify objects + // The MP API provides Net Ids for common objects (e.g. TrainCars, Jobs, Switches, Turntables and RailTrack), + // see `TryGetNetId(T obj, out ushort netId)` + internal class SimplePacketWithNetId : IPacket + { + public ushort CarNetId { get; set; } // example use of a Net Id used to identify a TrainCar + public Vector3 Position { get; set; } + public WheelArrangement WheelArrangement { get; set; } + } +} diff --git a/MultiplayerAPI Tests/TestComponents/ClientTest.cs b/MultiplayerAPI Tests/TestComponents/ClientTest.cs new file mode 100644 index 00000000..6f4bbabc --- /dev/null +++ b/MultiplayerAPI Tests/TestComponents/ClientTest.cs @@ -0,0 +1,204 @@ +using MPAPI; +using MPAPI.Interfaces; +using MultiplayerAPITest.Enums; +using MultiplayerAPITest.Packets; +using System; +using System.Text; +using UnityEngine; + +namespace MultiplayerAPITest.TestComponents; + +internal class ClientTest : MonoBehaviour +{ + const string LogPrefix = "ClientTest"; + const int DELAY_INTERVAL = 10; // 10 seconds + + IClient client; + + uint lastLogTick; + + protected void Awake() + { + client = MultiplayerAPI.Client; + + // Subscribe to game tick events + MultiplayerAPI.Instance.OnTick += OnTick; + + // Subscribe to player events + client.OnPlayerConnected += OnPlayerConnected; + client.OnPlayerDisconnected += OnPlayerDisconnected; + + // Subscribe to packets + Subscribe(); + + // Check if we are also a host - some mods may need to do nothing on a client-only game + // e.g. Clients should not generate jobs + if (MultiplayerAPI.Instance.IsHost) + { + if (MultiplayerAPI.Instance.IsDedicatedServer) + { + //Dedicated servers have not been implemented yet, IsDedicatedServer will always return false + Log("We are a dedicated server"); + } + else + { + var gameType = MultiplayerAPI.Instance.IsSinglePlayer ? "single player" : "multiplayer"; + Log($"We are in a {gameType} self-hosted game"); + } + } + else + { + Log("We are only a client game"); + } + } + + protected void Start() { } + + protected void Update() { } + + protected void OnDestroy() + { + // Unsubscribe from game tick events + MultiplayerAPI.Instance.OnTick -= OnTick; + + // Unsubscribe from player events + client.OnPlayerConnected -= OnPlayerConnected; + client.OnPlayerDisconnected -= OnPlayerDisconnected; + } + + private void Subscribe() + { + // Subscribe to network packets + // Note: only packets that will be received by client need to be registered here + client.RegisterPacket(OnTestSimpleModPacket); + client.RegisterPacket(OnSimplePacketWithNetId); + client.RegisterSerializablePacket(OnTestComplexModPacket); + } + + #region Example Tick Event + private void OnTick(uint tick) + { + // This event is called every tick + // This code is purely for testing purposes, not a recommened use case; normally it would be used for synchronising + // and batching changes or to track how long since an update has been received for a specific object. + + // The TICK_RATE is fixed at both client and server; currently the rate is 24 ticks/second + if ((tick - lastLogTick) > MultiplayerAPI.Instance.TICK_RATE * DELAY_INTERVAL) + { + //DELAY_INTERVAL (10 seconds) passed. + //log my ping + Log($"My current ping is {client.Ping} ms"); + + //Log the ping for all players + if (client.PlayerCount > 1) + { + StringBuilder sb = new($"Tick {tick}.\r\nThere are {client.PlayerCount} players, their pings are:"); + foreach (IPlayer player in client.Players) + sb.AppendLine($"\"{player?.PlayerId}\" {player.Ping} ms"); + + Log(sb.ToString()); + } + + lastLogTick = tick; + } + } + #endregion + + #region Player Events + private void OnPlayerConnected(IPlayer player) + { + // This event is called when another player connects + + Log($"Player \"{player?.PlayerId}\" has connected."); + } + + private void OnPlayerDisconnected(IPlayer player) + { + // This event is called when another player disconnects + + Log($"Player \"{player?.PlayerId}\" has connected."); + } + #endregion + + #region Packet Callbacks + //method called when a `TestSimplePacket` packet is received + private void OnTestSimpleModPacket(SimplePacket packet) + { + // We will just log this, but in a real use case you would validate the packet data, look up the referenced object and apply any updates required for your mod. + Log($"Received {packet.GetType()}, CarId: {packet.CarId}, Position: {packet.Position}, WheelArraangement: {packet.WheelArrangement}"); + + // For the purposes of testing and example, we will send the data back to the server + SendSimplePacket(packet.CarId, packet.Position, packet.WheelArrangement); + } + + //method called when a `TestSimplePacket` packet is received + private void OnSimplePacketWithNetId(SimplePacketWithNetId packet) + { + // Let's locate the car + + if (packet.CarNetId == 0) + { + LogWarning("Received SimplePacketWithNetId with a CarNetId of 0!"); + return; + } + + if (!MultiplayerAPI.Instance.TryGetObjectFromNetId(packet.CarNetId, out TrainCar car)) + { + LogWarning($"Received SimplePacketWithNetId with a CarNetId of {packet.CarNetId}, but TrainCar was not found!"); + return; + } + + Log($"Received {packet.GetType()}, CarNetId: {packet.CarNetId}, CarId: {car.ID}, Car Livery: {car.carLivery}, Position: {packet.Position}, Wheel Arrangement: {packet.WheelArrangement}"); + } + + //method called when a `TestComplexModPacket` packet is received + private void OnTestComplexModPacket(ComplexModPacket packet) + { + StringBuilder sb = new($"Received {packet.GetType()}\r\nPacket Data"); + + foreach (var kvp in packet.CarToPositionMap) + sb.AppendLine($"\tCarId: {kvp.Key}, Position: {kvp.Value}"); + + Log(sb.ToString()); + } + #endregion + + #region Packet Senders + public void SendSimplePacket(string carId, Vector3 position, WheelArrangement arrangement) + { + SimplePacket packet = new() + { + CarId = carId, + Position = position, + WheelArrangement = arrangement + }; + + //send the packet reliably + client.SendPacketToServer(packet, true); + } + #endregion + + #region Logging + + public void LogDebug(Func resolver) + { + MultiplayerAPITest.LogDebug(() => $"{LogPrefix} {resolver?.Invoke()}"); + } + + public void Log(object msg) + { + MultiplayerAPITest.Log($"{LogPrefix} {msg}"); + } + + public void LogWarning(object msg) + { + MultiplayerAPITest.LogWarning($"{LogPrefix} {msg}"); + } + + public void LogError(object msg) + { + MultiplayerAPITest.LogError($"{LogPrefix} {msg}"); + } + + #endregion +} diff --git a/MultiplayerAPI Tests/TestComponents/ServerTest.cs b/MultiplayerAPI Tests/TestComponents/ServerTest.cs new file mode 100644 index 00000000..e2e17897 --- /dev/null +++ b/MultiplayerAPI Tests/TestComponents/ServerTest.cs @@ -0,0 +1,496 @@ +using DV.Logic.Job; +using I2.Loc; +using MPAPI; +using MPAPI.Interfaces; +using MultiplayerAPITest.Enums; +using MultiplayerAPITest.Packets; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace MultiplayerAPITest.TestComponents; + +internal class ServerTest : MonoBehaviour +{ + const string LogPrefix = "ServerTest"; + const string MESSAGE_COLOUR_SERVER = "9CDCFE"; + const int DELAY_INTERVAL = 20; // seconds + + uint lastLogTick = 0; + + IServer server; + + protected void Awake() + { + server = MultiplayerAPI.Server; + + // Subscribe to game tick events + MultiplayerAPI.Instance.OnTick += OnTick; + + // Subscribe to player events + server.OnPlayerConnected += OnPlayerConnected; + server.OnPlayerDisconnected += OnPlayerDisconnected; + server.OnPlayerReady += OnPlayerReady; + + // Subscribe to packets and chat commands + Subscribe(); + } + + protected void Start() { } + + protected void Update() { } + + protected void OnDestroy() + { + // Unsubscribe from game tick events + MultiplayerAPI.Instance.OnTick -= OnTick; + + // Unsubscribe from player events + server.OnPlayerConnected -= OnPlayerConnected; + server.OnPlayerDisconnected -= OnPlayerDisconnected; + server.OnPlayerReady -= OnPlayerReady; + } + + private void Subscribe() + { + // Subscribe to network packets - note: only packets that will be received by server need to be registered here + server.RegisterPacket(OnTestSimpleModPacket); + server.RegisterPacket(OnSimplePacketWithNetId); + server.RegisterSerializablePacket(OnTestComplexModPacket); + + // Subscribe to chat commands - these have been added for API testing and examples + server.RegisterChatCommand("packet", "p", OnChatCommandSendPacketHelp, OnChatCommandSendPacket); //this command allows testing of simple packet sending + server.RegisterChatCommand("locopos", "lp", OnChatCommandSendLocoPosHelp, OnChatCommandSendLocoPos); //this command allows testing of complex packet sending + server.RegisterChatCommand("closest", "cd", OnChatCommandClosestPlayerHelp, OnChatCommandClosestPlayer); //this command returns the distance of the closest player to a given TrainCar + server.RegisterChatCommand("stats", null, OnChatCommandStatsHelp, OnChatCommandStats); //this command returns the number of connected players and all player names + + // Subscribe to chat filters + server.RegisterChatFilter(OnChatMessage); + } + + #region Example Tick Event + private void OnTick(uint tick) + { + // This event is called every tick + // This code is purely for testing purposes, not a recommened use case; normally it would be used for synchronising + // and batching changes or to track how long since an update has been received for a specific object. + + // The TICK_RATE is fixed at both client and server; currently the rate is 24 ticks/second + if ((tick - lastLogTick) > MultiplayerAPI.Instance.TICK_RATE * DELAY_INTERVAL) + { + //Log the ping for all players + if (server.PlayerCount == 0) + { + Log($"Tick {tick}.\r\nThere are no players connected"); + } + + StringBuilder sb = new($"Tick {tick}.\r\nThere are {server.PlayerCount} players, their pings are:"); + foreach (IPlayer player in server.Players) + sb.AppendLine($"\"{player?.PlayerId}\" {player.Ping} ms"); + + Log(sb.ToString()); + + lastLogTick = tick; + } + } + #endregion + + #region Player Events + private void OnPlayerConnected(IPlayer player) + { + // Send mod settings, parameters, etc. + // Note: This event occurs when the player is authenticated and before the player receives game state info + + Log($"Player {player?.PlayerId} (\"{player?.Username}\") has connected. (Is Loaded: {player?.IsLoaded})"); + } + + private void OnPlayerReady(IPlayer player) + { + // Player has indicated the world is loaded and they are ready to receive game state info + // Note: This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. + + Log($"Player \"{player?.PlayerId}\" is ready. (Is Loaded: {player?.IsLoaded})"); + + //Send an anouncement to all players + server.SendServerChatMessage($"Please welcome our newest driver {player?.PlayerId}!"); + } + + private void OnPlayerDisconnected(IPlayer player) + { + // Player has disconnected + // Note: This event occurs immediately prior to destroying the player object + // Complete all cleanup prior to returning from this method + + Log($"Player \"{player?.Username}\" has disconnected"); + } + #endregion + + #region Packet Callbacks + + // Method called when a `SimplePacket` packet is received + private void OnTestSimpleModPacket(SimplePacket packet, IPlayer player) + { + Log($"Received {packet.GetType()} from player: {player.Username}, CarId: {packet.CarId}, Position: {packet.Position}, WheelArraangement: {packet.WheelArrangement}"); + } + + // Method called when a `SimplePacketWithNetId` packet is received + private void OnSimplePacketWithNetId(SimplePacketWithNetId packet, IPlayer player) + { + Log($"Received {packet.GetType()} from player: {player.Username}, CarId: {packet.CarNetId}, Position: {packet.Position}, Wheel Arrangement: {packet.WheelArrangement}"); + } + + //method called when a `ComplexModPacket` packet is received + private void OnTestComplexModPacket(ComplexModPacket packet, IPlayer player) + { + StringBuilder sb = new($"Received {packet.GetType()}\r\nPacket Data"); + + foreach (var kvp in packet.CarToPositionMap) + sb.AppendLine($"\tCarId: {kvp.Key}, Position: {kvp.Value}"); + + Log(sb.ToString()); + } + #endregion + + #region Packet Senders + public void SendSimplePacketToAll(string carId, Vector3 position, WheelArrangement arrangement, IPlayer excludePlayer = null) + { + SimplePacket packet = new() + { + CarId = carId, + Position = position, + WheelArrangement = arrangement + }; + + // Send the packet reliably (ensure it makes it to all players), allow sending to the local client (true will block sending to a local client), exclude a player if specified + server.SendPacketToAll(packet, true, false, excludePlayer); + } + + public void SendSimplePacketWithNetIdToAll(ushort carId, Vector3 position, WheelArrangement arrangement, IPlayer excludePlayer = null) + { + + SimplePacketWithNetId packet = new() + { + CarNetId = carId, + Position = position, + WheelArrangement = arrangement + }; + + //send the packet reliably (ensure it makes it to all players) + server.SendPacketToAll(packet, true, true, excludePlayer); + } + + public void SendComplexPacket(Dictionary carToPos, IPlayer excludePlayer = null) + { + + ComplexModPacket packet = new() + { + CarToPositionMap = carToPos + }; + + // Send the packet reliably (ensure it makes it to all players), allow sending to the local client (true will block sending to a local client), exclude a player if specified + server.SendSerializablePacketToAll(packet, true, false, excludePlayer); + } + #endregion + + #region Chat Command Callbacks + // Called when a player uses the chat command '/packet' or '/p' + private void OnChatCommandSendPacket(string message, IPlayer sender) + { + string[] args = message.Split(' '); + string whisper; + + if (args.Length < 2) + { + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but not enough arguments were specified. Command: {message}"); + return; + } + + if (string.IsNullOrEmpty(args[0])) + { + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the first argument is empty. Command: {message}"); + whisper = $"Not enough arguments supplied. Type /? for help."; + server.SendWhisperChatMessage(whisper, sender); + return; + } + + if (string.IsNullOrEmpty(args[1])) + { + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the second argument is empty. Command: {message}"); + } + + LogDebug(() => $"OnChatCommandSendPacket({message}, {sender?.Username}) post-args checks"); + + var tc = GetTrainCarFromID(args[1].ToUpper()); + + if (tc == null) + { + // Send a whisper back to the player who sent the command + whisper = $"TrainCar '{args[1]}' not found"; + server.SendWhisperChatMessage(whisper, sender); + return; + } + + var pos = tc.transform.position - WorldMover.currentMove; + + switch (args[0].ToLower()) + { + case "simple": //send a simple packet + // Send a simple packet to all players using TrainCar id as a string, TrainCar position and a random wheel arrangement + SendSimplePacketToAll(args[1], pos, GetRandomWheelArrangement()); + whisper = $"Sending simple packet for '{args[1]}'"; + break; + + case "net": //send a simple packet using a netId + + if (MultiplayerAPI.Instance.TryGetNetId(tc, out ushort netId)) + { + // Send a simple packet to all players using TrainCar NetId, TrainCar position and a random wheel arrangement + SendSimplePacketWithNetIdToAll(netId, pos, GetRandomWheelArrangement()); + whisper = $"Sending net packet for '{args[1]}'"; + } + else + { + whisper = $"NetId not found for TrainCar '{args[1]}'"; + } + + break; + + default: + LogWarning($"Received 'SendPacket' chat command from player \"{sender.Username}\", but the packet type '{args[0].ToLower()}' was not recognised. Command: {message}"); + + // Send a whisper back to the player who sent the command + whisper = $"Packet type '{args[0].ToLower()}' was not recognised"; + + break; + } + + server.SendWhisperChatMessage(whisper, sender); + } + + // Called when a player uses the chat command '/locopos' or '/lp' + private void OnChatCommandSendLocoPos(string message, IPlayer sender) + { + //this chat command has no arguments + Dictionary carMap = []; + + foreach (var kvp in TrainCarRegistry.Instance.logicCarToTrainCar) + { + Car logicCar = kvp.Key; + TrainCar trainCar = kvp.Value; + + //locos only + if (!trainCar.IsLoco) + continue; + + if (!string.IsNullOrEmpty(logicCar.ID) && trainCar != null) + carMap[logicCar.ID] = trainCar.transform.position - WorldMover.currentMove; + } + + if (carMap.Count > 0) + SendComplexPacket(carMap); + + var whisper = $"Loco Position packet sent"; + server.SendWhisperChatMessage(whisper, sender); + } + + private void OnChatCommandClosestPlayer(string message, IPlayer sender) + { + string[] args = message.Split(' '); + string whisper; + + if (args.Length < 1) + { + LogWarning($"Received 'ClosestPlayer' chat command from player \"{sender.Username}\", but not enough arguments were specified. Command: {message}"); + return; + } + + if (string.IsNullOrEmpty(args[0])) + { + LogWarning($"Received 'ClosestPlayer' chat command from player \"{sender.Username}\", but the second argument is empty. Command: {message}"); + } + + var tc = GetTrainCarFromID(args[0].ToUpper()); + + if (tc != null) + { + // Check the distance between all players and the TrainCar + float closestSq = server.AnyPlayerSqrMag(tc.gameObject); + float closest = Mathf.Sqrt(closestSq); + + // Send a whisper back to the player who sent the command + whisper = $"The closest player to {tc.ID} is {closest:F2} metres away"; + } + else + { + whisper = $"TrainCar '{args[0]}' not found"; + } + + server.SendWhisperChatMessage(whisper, sender); + } + + private void OnChatCommandStats(string message, IPlayer sender) + { + StringBuilder whisper = new($"There {(server.PlayerCount > 1 ? "are" : "is")} {server.PlayerCount} connected player{(server.PlayerCount > 1 ? "s" : "")}:"); + + foreach (var player in server.Players) + whisper.Append($"
\t{(player.IsHost ? "" : "")}{player.Username}{(player.IsHost ? "" : "")} Id: {player.PlayerId}, Ping: {player.Ping}{(player.IsOnCar ? $", Riding {player.OccupiedCar.ID}" : "")}"); + + whisper.Append(""); + + server.SendWhisperChatMessage(whisper.ToString(), sender); + } + #endregion + + #region Chat Help Callbacks + private string OnChatCommandSendPacketHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Aktiviere den Server um ein Testpaket zu senden" + + "\r\n\t\t/packet " + + "\r\n\t\t/p " + + "\r\n\t\t/packet simple L-025", + + "Italian" => "Attiva il server per inviare un pacchetto di prova" + + "\r\n\t\t/packet " + + "\r\n\t\t/p " + + "\r\n\t\t/packet simple L-025", + + _ => "Trigger server to send a test packet" + + "\r\n\t\t/packet " + + "\r\n\t\t/p " + + "\r\n\t\t/packet simple L-025", + }; + } + private string OnChatCommandSendLocoPosHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Aktiviere den Server um ein Paket mit der Lokomotive und ihrer Position zu senden" + + "\r\n\t\t/locopos" + + "\r\n\t\t/lp", + + "Italian" => "Attiva il server per inviare un pacchetto complesso di auto e le loro posizioni" + + "\r\n\t\t/locopos" + + "\r\n\t\t/lp", + + _ => "Trigger server to send a complex packet of cars and their positions" + + "\r\n\t\t/locopos" + + "\r\n\t\t/lp", + }; + } + + private string OnChatCommandClosestPlayerHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Aktiviere den Server um die Entfernung zwischen dem Spieler und dem nächsten Auto zu senden" + + "\r\n\t\t/closest " + + "\r\n\t\t/cd ", + + "Italian" => "Restituisce la distanza tra un dato vagone e il giocatore più vicino" + + "\r\n\t\t/closest " + + "\r\n\t\t/cd ", + + _ => "Returns the distance between a given TrainCar and the closest player" + + "\r\n\t\t/closest " + + "\r\n\t\t/cd ", + }; + } + + private string OnChatCommandStatsHelp() + { + // this is a very basic example and a better localisation system should be used + return LocalizationManager.CurrentLanguage switch + { + "German" => "Gibt die Spieleranzahl und eine Liste aller verbundenen Spieler zurück" + + "\r\n\t\t/stats", + + "Italian" => "Restituisce il conteggio dei giocatori e l'elenco di tutti i giocatori connessi" + + "\r\n\t\t/stats", + + _ => "Returns player count and list of all connected players" + + "\r\n\t\t/stats", + }; + } + + #endregion + + #region Chat Message Filters + + //simple swear filter (not intended for real use) - this is a basic example of a chat filter,but could be used for much more. + private bool OnChatMessage(ref string message, IPlayer sender) + { + string[] veryBadWords = { "poo", "loser" }; + string[] moderatelyBadWords = { "bum", "dumb" }; + + //check for very bad words - block the message entirely if found + string localMessage = message; + if (veryBadWords.Any(word => localMessage.IndexOf(word, StringComparison.OrdinalIgnoreCase) >= 0)) + { + //send a whisper back to the player + var whisper = $"Please do not swear on this server"; + server.SendWhisperChatMessage(whisper, sender); + + //block the message from being sent + return false; + } + + //check for moderately bad words - allow the message but replace with astersiks + foreach (string badWord in moderatelyBadWords) + { + var badWordstart = message.IndexOf(badWord, StringComparison.OrdinalIgnoreCase); + while (badWordstart >= 0) + { + message = message.Remove(badWordstart, badWord.Length).Insert(badWordstart, new string('*', badWord.Length)); + badWordstart = message.IndexOf(badWord, badWordstart + badWord.Length, StringComparison.OrdinalIgnoreCase); + } + } + + return true; + } + #endregion + + #region helpers + private TrainCar GetTrainCarFromID(string carId) + { + return TrainCarRegistry.Instance.logicCarToTrainCar.FirstOrDefault(kvp => kvp.Value.ID == carId).Value; + } + + private WheelArrangement GetRandomWheelArrangement() + { + var values = Enum.GetValues(typeof(WheelArrangement)); + var random = new System.Random(); + return (WheelArrangement)values.GetValue(random.Next(values.Length)); + } + #endregion + + #region Logging + + public void LogDebug(Func resolver) + { + MultiplayerAPITest.LogDebug(() => $"{LogPrefix} {resolver?.Invoke()}"); + } + + public void Log(object msg) + { + MultiplayerAPITest.Log($"{LogPrefix} {msg}"); + } + + public void LogWarning(object msg) + { + MultiplayerAPITest.LogWarning($"{LogPrefix} {msg}"); + } + + public void LogError(object msg) + { + MultiplayerAPITest.LogError($"{LogPrefix} {msg}"); + } + + #endregion +} diff --git a/MultiplayerAPI/Interfaces/IClient.cs b/MultiplayerAPI/Interfaces/IClient.cs new file mode 100644 index 00000000..fcecc78c --- /dev/null +++ b/MultiplayerAPI/Interfaces/IClient.cs @@ -0,0 +1,117 @@ +using MPAPI.Interfaces.Packets; +using MPAPI.Types; +using System; +using System.Collections.Generic; +using static UnityModManagerNet.UnityModManager; + +namespace MPAPI.Interfaces; + +/// +/// Interface for interacting with Multiplayer mod client instances. +/// +public interface IClient +{ + /// + /// Event fired when a player connects. + /// + /// + /// The event handler receives an object for the connected player. + /// + event Action OnPlayerConnected; + + /// + /// Event fired when a player disconnects, but before the object is destroyed. + /// + /// + /// The event handler receives an object for the disconnected player. + /// + event Action OnPlayerDisconnected; + + /// + /// Registers a block to prevent the client from sending the 'Ready' signal to the server until all mods have called 'CancelReadyBlock'. + /// + /// Mod information. + /// + /// Only required if the mod needs complete loading prior to receiving game state from the server. + /// + void RegisterReadyBlock(ModInfo modInfo); + + /// + /// Cancels a previously registered ready block. + /// + /// Mod information. + /// + /// All registered blocks must be cancelled prior to the client sending the 'Ready' signal to the server. + /// + void CancelReadyBlock(ModInfo modInfo); + + /// + /// Gets Player Id of the local player. + /// + /// + /// The local player does not have an object. + /// + byte PlayerId { get; } + + /// + /// Gets objects for all players connected to the server. + /// + /// Read-only collection of objects. + IReadOnlyCollection Players { get; } + + /// + /// Gets number of players currently connected to the server. + /// + /// Positive integer representing the number of connected players. + int PlayerCount { get; } + + /// + /// Gets the for player by Id. + /// + /// object if found, otherwise null. + IPlayer GetPlayer(byte playerId); + + /// + /// Gets connection state for the client. + /// + bool IsConnected { get; } + + /// + /// Gets ping for the client. + /// + int Ping { get; } + + #region Packet API + /// + /// Register a packet type that uses automatic serialisation. + /// + /// Packet type implementing . + /// Handler to call when packet is received. + void RegisterPacket(ClientPacketHandler handler) where T : class, IPacket, new(); + + /// + /// Register a packet type that uses manual serialisation. + /// + /// Packet type implementing . + /// Handler to call when packet is received. + void RegisterSerializablePacket(ClientPacketHandler handler) where T : class, ISerializablePacket, new(); + + + /// + /// Send a packet based on to the server. + /// + /// Packet type. + /// Packet to send. + /// Whether to send reliably. + void SendPacketToServer(T packet, bool reliable = true) where T : class, IPacket, new(); + + /// + /// Send a packet based on to the server. + /// + /// Packet type. + /// Packet to send. + /// Whether to send reliably. + void SendSerializablePacketToServer(T packet, bool reliable = true) where T : class, ISerializablePacket, new(); + + #endregion +} diff --git a/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs new file mode 100644 index 00000000..ce48a8fc --- /dev/null +++ b/MultiplayerAPI/Interfaces/IMultiplayerAPI.cs @@ -0,0 +1,175 @@ +using DV.Customization.Paint; +using DV.Logic.Job; +using MPAPI.Types; +using System; +using System.Collections.Generic; + +namespace MPAPI.Interfaces; + +/// +/// Main interface for interacting with the Multiplayer mod. +/// +public interface IMultiplayerAPI +{ + /// + /// Gets the version of the Multiplayer API that the Multiplayer mod supports. + /// + public string SupportedApiVersion { get; } + + /// + /// Gets the version of the Multiplayer mod itself. + /// + public string MultiplayerVersion { get; } + + /// + /// Gets whether the multiplayer mod is currently loaded and active. + /// + bool IsMultiplayerLoaded { get; } + + /// Sets the mod's compatibility requirements. + /// String representing the your mod's Id (`ModEntry.Info.Id`). + /// ModCompatibility flags representing installation host/client requirements. + void SetModCompatibility(string modId, MultiplayerCompatibility compatibility); + + /// + /// Returns true if either a host or client exist. + /// + bool IsConnected { get; } + + /// + /// Gets whether this instance is host. + /// + bool IsHost { get; } + + /// + /// Gets whether this instance is a dedicated server. + /// + bool IsDedicatedServer { get; } + + /// + /// Gets whether this current session is single player. + /// + bool IsSinglePlayer { get; } + + /// + /// Event fired when a game/network tick occurs. + /// Ticks occur at a fixed interval (TICK_INTERVAL = 1/TICK_RATE) and are useful for synchronisation, batching, and processing changes. + /// + /// The tick parameter can be used to determine if non-reliable packets have been dropped and to sequence actions for rollbacks or preventing stale data from being processed. + /// + /// Example: In Multiplayer's TrainCar simulation sync, small changes are cached when they occur but sent as a single packet per TrainCar when OnTick fires, reducing network overhead. + /// + /// The event handler receives a representing the current tick number. + event Action OnTick; + + /// + /// The number of ticks per second (currently 24). + /// Used to calculate the fixed tick interval: TICK_INTERVAL = 1.0f / TICK_RATE. + /// + uint TICK_RATE { get; } + + /// + /// The current game tick. + /// + uint CurrentTick { get; } + + /// + /// Gets the NetId for an object. + /// + /// The object you want the NetId for. + /// When this method returns, contains the NetId associated with the specified object, if found; otherwise, 0. + /// True if a NetId for the object was found; otherwise, false. + bool TryGetNetId(T obj, out ushort netId) where T : class; + + /// + /// Gets the object for a NetId. + /// + /// The non-zero NetId for the object. + /// When this method returns, contains the object associated with the NetId, if found; otherwise null. + /// True if the object was found; otherwise, false. + bool TryGetObjectFromNetId(ushort netId, out T obj) where T : class; + + /// + /// Gets the NetId for an object. + /// + /// The object you want the NetId for. + /// When this method returns, contains the NetId associated with the specified object, if found; otherwise, 0. + /// True if a NetId for the object was found; otherwise, false. + bool TryGetNetId(T obj, out uint netId) where T : class; + + /// + /// Gets the object for a NetId. + /// + /// The non-zero NetId for the object. + /// When this method returns, contains the object associated with the NetId, if found; otherwise null. + /// True if the object was found; otherwise, false. + bool TryGetObjectFromNetId(uint netId, out T obj) where T : class; + + /// + /// Registers a PaintTheme and returns its netId. + /// + /// The to be registered. + /// Non-zero, unique Id if the theme was successfully registered, otherwise 0. + /// PaintThemes must be registered each time the client or server starts, registration is not persistent across sessions. + uint RegisterPaintTheme(PaintTheme theme); + + /// + /// Unregisters a PaintTheme. + /// + /// The to be unregistered. + /// Base game PaintThemes cannot be unregistered. + void UnregisterPaintTheme(PaintTheme theme); + + /// + /// Registers a serialiser/deserialiser for a custom type for multiplayer synchronisation. + /// + /// The concrete type to register. + /// + /// The type that handles serialisation and deserialisation for . + /// Must have a parameterless constructor and implement to convert from the task. + /// + /// The enum value associated with this task type. + /// + /// true if the task type was successfully registered; false if the task type was already registered or registration failed. + /// + /// + /// This method automatically handles conversion by instantiating , calling its + /// method for serialisation, and creating empty instances for deserialisation. + /// + bool RegisterTaskType(TaskType taskType) where TCustomTask : Task where TTaskNetworkData : TaskNetworkData, new(); + + /// + /// Unregisters a previously registered custom type. + /// + /// The concrete type to unregister. + /// The enum value associated with the task type to unregister. + /// + /// true if the task type was successfully unregistered; false if the task type was not found or is a base-game task type. + /// + /// + /// This method allows removal of custom or extended task types from the multiplayer system. + /// Base-game task types cannot be unregistered. + /// + bool UnregisterTaskType(TaskType taskType) where TCustomTask : Task; + + /// + /// Converts an IEnumerable collection of into an array of . + /// + /// The collection of tasks to convert. + /// An array of representing the tasks. + TaskNetworkData[] ConvertTasks(IEnumerable tasks); + + /// + /// Converts a into a . + /// + /// The task to convert. + /// A representing the task. + TaskNetworkData ConvertTask(Task task); + + /// + /// Retrieves a for the specified . + /// + /// The task type to convert. + /// A representing the task. + TaskNetworkData ConvertTask(TaskType taskType); +} diff --git a/MultiplayerAPI/Interfaces/INetId.cs b/MultiplayerAPI/Interfaces/INetId.cs new file mode 100644 index 00000000..94ed56df --- /dev/null +++ b/MultiplayerAPI/Interfaces/INetId.cs @@ -0,0 +1,78 @@ + +namespace MPAPI.Interfaces; + +/// +/// Provides methods for mapping between built-in game objects and their network identifiers in the Multiplayer system. +/// +/// +/// This interface enables bidirectional lookup between game objects and their corresponding network IDs, +/// which are used to synchronise object references across the network. Only objects that are actively +/// synchronised by Multiplayer mod will have associated network identifiers. +/// +/// Additional objects from the base-game will be added as Multiplayer features are implemented. If there are +/// specific object types you would like to see supported, please create an issue on the Multiplayer Mod GitHub repository. +/// +public interface INetIdProvider +{ + /// + /// Attempts to retrieve the network identifier for the specified object. + /// + /// The type of object to get the network ID for. Must be a reference type. + /// The object to get the network identifier for. + /// + /// When this method returns, contains the network identifier associated with the object if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the network identifier was successfully retrieved; otherwise, false. + /// + bool TryGetNetId(T obj, out ushort netId) where T : class; + + /// + /// Attempts to retrieve the object associated with the specified network identifier. + /// + /// The type of object to retrieve. Must be a reference type. + /// The network identifier of the object to retrieve. + /// + /// When this method returns, contains the object associated with the network identifier if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the object was successfully retrieved; otherwise, false. + /// + bool TryGetObject(ushort netId, out T obj) where T : class; + + /// + /// Attempts to retrieve the network identifier for the specified object. + /// + /// The type of object to get the network ID for. Must be a reference type. + /// The object to get the network identifier for. + /// + /// When this method returns, contains the network identifier associated with the object if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the network identifier was successfully retrieved; otherwise, false. + /// + /// + /// This method is for network identifiers represented as unsigned 32-bit integers, and is typically used where V2 types are involved. + /// + bool TryGetNetId(T obj, out uint netId) where T : class; + + /// + /// Attempts to retrieve the object associated with the specified network identifier. + /// + /// The type of object to retrieve. Must be a reference type. + /// The network identifier of the object to retrieve. + /// + /// When this method returns, contains the object associated with the network identifier if found; + /// otherwise, the default value for the type. + /// + /// + /// true if the object was successfully retrieved; otherwise, false. + /// + /// + /// This method is for network identifiers represented as unsigned 32-bit integers, and is typically used where V2 types are involved. + /// + bool TryGetObject(uint netId, out T obj) where T : class; +} diff --git a/MultiplayerAPI/Interfaces/IPlayer.cs b/MultiplayerAPI/Interfaces/IPlayer.cs new file mode 100644 index 00000000..5c37184b --- /dev/null +++ b/MultiplayerAPI/Interfaces/IPlayer.cs @@ -0,0 +1,69 @@ +using System; +using UnityEngine; + +namespace MPAPI.Interfaces +{ + /// + /// Represents a player in the multiplayer session, providing access to player state and information. + /// + public interface IPlayer + { + /// + /// Gets the identifier for the player within the session. + /// + /// + /// This identifier can be used as a network ID for referencing the player across the network. + /// If the player leaves the session the Id will be reassigned to the next player to join. + /// + public byte PlayerId { get; } + + /// + /// Gets the username/display name of the player. + /// + public string Username { get; } + + /// + /// Gets the current world position of the player. + /// + Vector3 Position { get; } + + /// + /// Gets the current Y-axis rotation of the player. + /// + float RotationY { get; } + + /// + /// Gets a value indicating whether the player has finished loading the game world. + /// + /// + /// true if the player has completed world loading and is ready to receive game state updates; otherwise, false. + /// + bool IsLoaded { get; } + + /// + /// Gets a value indicating whether this player is the host of the multiplayer session. + /// + /// true if the player is the session host; otherwise, false. + bool IsHost { get; } + + /// + /// Gets the current network ping/latency for this player. + /// + /// The one-way time in milliseconds between the server and this player. + int Ping { get; } + + /// + /// Gets a value indicating whether this player is on a car. + /// + /// true if the player is on a car; otherwise, false. + bool IsOnCar { get; } + + /// + /// Gets the train car that the player is currently occupying. + /// + /// + /// The instance the player is on, or null if the player is not on any car. + /// + TrainCar OccupiedCar { get; } + } +} diff --git a/MultiplayerAPI/Interfaces/IServer.cs b/MultiplayerAPI/Interfaces/IServer.cs new file mode 100644 index 00000000..55f25a69 --- /dev/null +++ b/MultiplayerAPI/Interfaces/IServer.cs @@ -0,0 +1,184 @@ +using MPAPI.Interfaces.Packets; +using MPAPI.Types; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace MPAPI.Interfaces; + +/// +/// Represents the method that will handle chat command execution. +/// +/// The message content without the command prefix (e.g. '/command parameter1 parameter2' becomes 'parameter1 parameter2'). +/// The player who executed the command. +public delegate void ChatCommandCallback(string message, IPlayer sender); + +/// +/// Represents the method that will handle chat message filtering. +/// +/// The message content that can be modified by reference. +/// The player who sent the message. +/// True to pass the message to the next filter or send to players; false to block the message from further processing. +public delegate bool ChatFilterDelegate(ref string message, IPlayer sender); + +/// +/// Interface for interacting with Multiplayer mod server instances. +/// +public interface IServer +{ + + /// + /// Event fired when a player connects and is authenticated, but before the player receives game state information. + /// + /// + /// The event handler receives an object for the connected player. This event is not triggered for the local-client player on the host. + /// + event Action OnPlayerConnected; + + /// + /// Event fired when a player disconnects, but before the object is destroyed. + /// + /// + /// The event handler receives an object for the connected player. + /// + event Action OnPlayerDisconnected; + + /// + /// Event fired when a player has signalled they are ready for game state information. + /// + /// + /// This event occurs after the server has sent the game state, it does not guarantee the player has finished generating all cars, jobs, etc. + /// + event Action OnPlayerReady; + + #region Server Properties + /// + /// Gets number of players currently connected to the server. + /// + /// Positive integer representing the number of connected players. + int PlayerCount { get; } + + /// + /// Gets objects for all players connected to the server. + /// + /// Read-only collection of objects. + public IReadOnlyCollection Players { get; } + + /// + /// Gets for player by Id. + /// + /// Id for the player. + /// object if found, otherwise null. + IPlayer GetPlayer(byte id); + + #endregion + + #region Packet API + /// + /// Register a packet type that uses automatic serialisation. + /// + /// Packet type implementing . + /// Handler to call when packet is received. + void RegisterPacket(ServerPacketHandler handler) where T : class, IPacket, new(); + + /// + /// Register a packet type that uses manual serialisation. + /// + /// Packet type implementing . + /// Handler to call when packet is received. + void RegisterSerializablePacket(ServerPacketHandler handler) where T : class, ISerializablePacket, new(); + + + /// + /// Send a packet based on to all connected players. + /// + /// Packet type. + /// Packet to send. + /// Whether to send reliably. + /// Sends the packet to the local client when false, skips sending to the local client when true. + /// To be excluded from this broadcast. + void SendPacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, IPacket, new(); + + /// + /// Send a packet based on to all connected players. + /// + /// Packet type. + /// Packet to send. + /// Whether to send reliably. + /// Sends the packet to the local client when false, skips sending to the local client when true. + /// To be excluded from this broadcast. + void SendSerializablePacketToAll(T packet, bool reliable = true, bool excludeSelf = false, IPlayer excludePlayer = null) where T : class, ISerializablePacket, new(); + + /// + /// Send a packet based on to a specific player. + /// + /// Packet type. + /// Packet to send. + /// Target player. + /// Whether to send reliably. + void SendPacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, IPacket, new(); + + /// + /// Send a packet based on to a specific player. + /// + /// Packet type. + /// Packet to send. + /// Target player. + /// Whether to send reliably. + void SendSerializablePacketToPlayer(T packet, IPlayer player, bool reliable = true) where T : class, ISerializablePacket, new(); + #endregion + + #region Server Util + /// + /// Returns the distance (Square Magnitude) of the closest player to a given GameObject. + /// + /// GameObject to compare players against. + /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby. + float AnyPlayerSqrMag(GameObject gameObject); + + /// + /// Returns the distance (Square Magnitude) of the closest player to a given point. + /// + /// Anchor point to compare players against. + /// Returns the distance (Square Magnitude) of the closest player, or float.MaxValue if no player is nearby. + float AnyPlayerSqrMag(Vector3 anchor); + #endregion + + #region Chat API + /// + /// Sends a server chat message. + /// + /// Message to be sent. + /// Player to exclude. If null, message will go to all players. + /// Server chat messages are messsages sent from the server, not from a specific player and will be stylised as such. + void SendServerChatMessage(string message, IPlayer excludePlayer = null); + + /// + /// Sends a chat message to a specific player. + /// + /// Message to be sent. + /// Recipient player. + void SendWhisperChatMessage(string message, IPlayer player); + + /// + /// Registers a chat command e.g. `/server` and optional short command '/s'. + /// + /// Command to be filtered for, without a leading '/' e.g. 'server'. + /// Optional short command to be filtered for, without a leading '/' e.g. 's'. + /// Optional callback for a help message e.g. \r\n\t\t/s "]]>. It is recommended to provide localisation/translation for this string. + /// Action to execute when the command is triggered. First parameter contains message without the command e.g. '/command parameter1 parameter2' will become 'parameter1 parameter2', second parameter is the player who executed the command. + /// True if the command was successfully registered, false if registration failed (e.g. command already exists). + bool RegisterChatCommand(string commandLong, string commandShort, Func helpMessage, ChatCommandCallback callback); + + + /// + /// Registers a chat filter that processes non-command messages in registration order. + /// Filters form a chain where each filter can either allow the message to continue to the next filter or block further processing. + /// If all filters return true, the message will be sent to all players (default action). + /// This filter also applies to whispered messages, regardless of source (player or server). + /// + /// Filter function type `ChatFilterDelegate` that processes the message. First delegate parameter is the message content, second parameter is the player who sent the message. Return true to pass the message to the next filter/default action, false to block propagation. + void RegisterChatFilter(ChatFilterDelegate callback); + + #endregion +} diff --git a/MultiplayerAPI/Interfaces/Packets/IPacket.cs b/MultiplayerAPI/Interfaces/Packets/IPacket.cs new file mode 100644 index 00000000..83db49e2 --- /dev/null +++ b/MultiplayerAPI/Interfaces/Packets/IPacket.cs @@ -0,0 +1,9 @@ +namespace MPAPI.Interfaces.Packets; + +/// +/// Base interface for packets using automatic serialisation. +/// +public interface IPacket +{ + // Empty interface - Multiplayer Mod/LiteNetLib handles serialisation automatically for you via public properties. +} diff --git a/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs b/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs new file mode 100644 index 00000000..e5c082c0 --- /dev/null +++ b/MultiplayerAPI/Interfaces/Packets/ISerializablePacket.cs @@ -0,0 +1,22 @@ +using System.IO; + +namespace MPAPI.Interfaces.Packets; + +/// +/// Base interface for packets using manual serialisation. +/// Implementing classes must handle their own serialisation/deserialisation. +/// +public interface ISerializablePacket +{ + /// + /// Serialise the packet data to the provided . + /// + /// to serialise data to. + void Serialize(BinaryWriter writer); + + /// + /// Deserialise the packet data from the provided . + /// + /// to deserialise data from. + void Deserialize(BinaryReader reader); +} diff --git a/MultiplayerAPI/MultiplayerAPI.cs b/MultiplayerAPI/MultiplayerAPI.cs new file mode 100644 index 00000000..32b2cf61 --- /dev/null +++ b/MultiplayerAPI/MultiplayerAPI.cs @@ -0,0 +1,148 @@ +using MPAPI.Interfaces; +using System; +using System.Linq; +using System.Reflection; + +namespace MPAPI; + +/// +/// Provides an API interface for accessing Multiplayer Mod functionality and managing server/client instances. +/// +/// +/// This class serves as the main entry point for the Multiplayer API, providing events for server and client lifecycle management, +/// and access to the current server, client, and API instances. +/// +public static class MultiplayerAPI +{ + /// + /// Gets the version of the Multiplayer API DLL that is currently loaded. + /// + /// The version string of the API DLL. + public static string LoadedApiVersion + { + get + { + AssemblyInformationalVersionAttribute info = (AssemblyInformationalVersionAttribute)typeof(MultiplayerAPI).Assembly. + GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .FirstOrDefault(); + + if (info == null) + return ""; + + return info.InformationalVersion.Split('+')[0]; + } + } + + /// + /// Gets the version of the Multiplayer API that the Multiplayer mod supports. + /// + /// The supported API version string, or null if multiplayer is not loaded. + /// + /// This indicates the API version that the Multiplayer mod was built against and is compatible with. + /// If this differs from , there may be compatibility issues. + /// + public static string SupportedApiVersion => _instance?.SupportedApiVersion; + + /// + /// Gets the version of the Multiplayer mod itself. + /// + /// The Multiplayer mod version string, or null if multiplayer is not loaded. + public static string MultiplayerVersion => _instance?.MultiplayerVersion; + + /// + /// Event fired when a server instance has been created. + /// + /// + /// This event provides access to the instance that was started. + /// + public static event Action ServerStarted; + + /// + /// Event fired when a client instance has been created. + /// + /// + /// This event provides access to the instance that was started. + /// + public static event Action ClientStarted; + + /// + /// Event fired when a server instance is stopped. + /// + public static event Action ServerStopped; + + /// + /// Event fired when a client instance is stopped. + /// + public static event Action ClientStopped; + + private static IMultiplayerAPI _instance; + private static IServer _server; + private static IClient _client; + + /// + /// Gets whether the Multiplayer mod is available. + /// + public static bool IsMultiplayerLoaded => _instance != null; + + /// + /// Gets the current API instance (null if Multiplayer mod is not loaded). + /// + public static IMultiplayerAPI Instance => _instance; + + /// + /// Gets the current Server API instance (null if Multiplayer mod is not loaded or server not running). + /// + public static IServer Server => _server; + + /// + /// Gets the current Client API instance (null if Multiplayer mod is not loaded or client not running). + /// + public static IClient Client => _client; + + /// + /// Internal method for the Multiplayer mod to register itself. + /// + /// The API implementation. + internal static void RegisterAPI(IMultiplayerAPI apiInstance) + { + _instance = apiInstance; + } + + /// + /// Internal method for the Multiplayer mod to register a client instance. + /// + /// The Client implementation + internal static void RegisterClient(IClient client) + { + _client = client; + ClientStarted?.Invoke(client); + } + + /// + /// Internal method for the Multiplayer mod to deregister a client instance. + /// + internal static void ClearClient() + { + _client = null; + ClientStopped?.Invoke(); + } + + /// + /// Internal method for the Multiplayer mod to register a server instance. + /// + /// The API implementation. + internal static void RegisterServer(IServer server) + { + _server = server; + ServerStarted?.Invoke(server); + } + + /// + /// Internal method for the Multiplayer mod to deregister a server instance. + /// + internal static void ClearServer() + { + _server = null; + ServerStopped?.Invoke(); + } +} diff --git a/MultiplayerAPI/MultiplayerAPI.csproj b/MultiplayerAPI/MultiplayerAPI.csproj new file mode 100644 index 00000000..489bfb88 --- /dev/null +++ b/MultiplayerAPI/MultiplayerAPI.csproj @@ -0,0 +1,91 @@ + + + + net48 + latest + MPAPI + 1.0.0.0 + 1.0.0.0 + + + true + DVMultiplayerAPI + Derail Valley Multiplayer API + Macka + API for interfacing with DV Multiplayer mod. Provides events and interfaces for server/client interactions in Derail Valley multiplayer scenarios. + derail-valley;multiplayer;gaming;api;mod + https://github.com/AMacro/dv-multiplayer/wiki/API-Overview + https://github.com/AMacro/dv-multiplayer + git + Apache-2.0 + Initial release of DV Multiplayer API + false + README.md + + + true + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + true + snupkg + + + + + <_Parameter1>Multiplayer + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MultiplayerAPI/ReadMe.md b/MultiplayerAPI/ReadMe.md new file mode 100644 index 00000000..4146a87f --- /dev/null +++ b/MultiplayerAPI/ReadMe.md @@ -0,0 +1,10 @@ +# Derail Valley Multiplayer Mod API +API for interfacing with DV Multiplayer mod. Provides events and interfaces for server/client interactions in Derail Valley multiplayer scenarios. + +This package is licenced under Apache 2.0, please see the [repository](https://github.com/AMacro/dv-multiplayer) for the full licence and source code. + +For full documentation and examples, please see the [wiki](https://github.com/AMacro/dv-multiplayer/wiki/API-Overview). + +All issues should be reported in the repository's [issue tracker](https://github.com/AMacro/dv-multiplayer/issues). + +General support can be found in the [Multiplayer mod.](https://discord.com/channels/332511223536943105/1234574186161377363) thread on the [Altfuture Discord](https://discord.gg/7QKaeuHkKC) server. diff --git a/MultiplayerAPI/Types/MultiplayerCompatibility.cs b/MultiplayerAPI/Types/MultiplayerCompatibility.cs new file mode 100644 index 00000000..1de39d0e --- /dev/null +++ b/MultiplayerAPI/Types/MultiplayerCompatibility.cs @@ -0,0 +1,41 @@ + +namespace MPAPI.Types; + +/// +/// Defines how a mod works with multiplayer functionality. +/// +public enum MultiplayerCompatibility : byte +{ + + /// + /// Mod has not defined compatibility. + /// If the host is using this mod all clients must also have it. + /// If a client is using this mod and the host is not, the client will be unable to join the game. + /// + Undefined, + + /// + /// Mod is incompatible with multiplayer. + /// The mod must be disabled if Multiplayer Mod is enabled. + /// + Incompatible, + + /// + /// Mod must be installed on the host and all clients. + /// Players without this mod will be unable to join the game. + /// Mods are responsible for disabling behaviour when connecting to a host without the mod. + /// + All, + + /// + /// Mod must be installed on the host. + /// Mods are responsible for disabling their behaviour if the player is not the host. + /// + Host, + + /// + /// Mod has no effect on the game play and can be ignored. + /// This should be used for client-only mods e.g. GUI enhancements, controller mods, RUE, etc. + /// + Client, +} diff --git a/MultiplayerAPI/Types/PacketHandler.cs b/MultiplayerAPI/Types/PacketHandler.cs new file mode 100644 index 00000000..c95f1520 --- /dev/null +++ b/MultiplayerAPI/Types/PacketHandler.cs @@ -0,0 +1,18 @@ +using MPAPI.Interfaces; + +namespace MPAPI.Types; + +/// +/// Delegate for handling received packets on the server +/// +/// Packet type +/// The received packet +/// The player who sent the packet +public delegate void ServerPacketHandler(T packet, IPlayer sender) where T : class; + +/// +/// Delegate for handling received packets on the client +/// +/// Packet type +/// The received packet +public delegate void ClientPacketHandler(T packet) where T : class; diff --git a/MultiplayerAPI/Types/TaskNetworkDataType.cs b/MultiplayerAPI/Types/TaskNetworkDataType.cs new file mode 100644 index 00000000..046d8387 --- /dev/null +++ b/MultiplayerAPI/Types/TaskNetworkDataType.cs @@ -0,0 +1,170 @@ +using DV.Logic.Job; +using System.Collections.Generic; +using System.IO; + +namespace MPAPI.Types; + +#region TaskData Base Class +/// +/// Base class for serialising and deserialising job task data for transmission by Multiplayer mod. +/// Not intended for direct use; inherit via . +/// +public abstract class TaskNetworkData +{ + /// + /// Gets or sets the unique network identifier for this task within its job. + /// + public ushort TaskNetId { get; set; } + + /// + /// Gets or sets the current state of the task. + /// See for possible values. + /// + public TaskState State { get; set; } + + /// + /// Gets or sets the time at which the task started, in seconds since the job began. + /// + public float TaskStartTime { get; set; } + + /// + /// Gets or sets the time at which the task finished, in seconds since the job began. + /// + public float TaskFinishTime { get; set; } + + /// + /// Gets or sets a value indicating whether this is the last task in the job sequence. + /// + public bool IsLastTask { get; set; } + + /// + /// Gets or sets the time limit for completing the task, in seconds. + /// + public float TimeLimit { get; set; } + + /// + /// Gets or sets the type of the task. + /// See for possible values. + /// + public TaskType TaskType { get; set; } + + /// + /// Serialises the task network data to the specified . + /// Implementations should write all relevant fields for network transmission. + /// + /// + /// The first line of the implementation should call . + /// + /// The to write data to. + public abstract void Serialize(BinaryWriter writer); + + /// + /// Deserialises the task network data from the specified . + /// Implementations should read all relevant fields in the same order and size as written by . + /// + /// + /// The first line of the implementation should call . + /// + /// The to read data from. + public abstract void Deserialize(BinaryReader reader); + + /// + /// Converts this instance into a object + /// compatible with the job/task system, and adds them to the provided dictionary. + /// + /// + /// A reference to a that will be populated with deserialized instances. + /// Each key is a netTaskId (ushort), and each value is the corresponding object. + /// + /// A instance representing the deserialized data. + /// + /// Implementations should add all relevant instances to . + /// This allows aggregation of multiple tasks from different objects into a single dictionary. + /// + public abstract Task ToTask(ref Dictionary netIdToTask); + + /// + /// Gets a list of car IDs () associated with this task. + /// + /// A list of car IDs relevant to the task. + public abstract List GetCars(); +} + +/// +/// Generic abstract base class providing type-safe conversion for serialising and deserialising job task data. +/// Inherit from this class to implement serialisers for custom types. +/// +/// The concrete type that inherits from this class. +public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData +{ + /// + /// Populates this instance from the specified object. + /// + /// The to extract data from. + /// This method is called by Multiplayer mod when serialising a job. + /// This instance, populated with data from the provided task. + public abstract T FromTask(Task task); + + /// + /// Extracts and populates the common task data fields from the specified object. + /// Should be called as the first step in the FromTask implementation of derived classes. + /// + /// The to extract data from. + protected void FromTaskCommon(Task task) + { + State = task.state; + TaskStartTime = task.taskStartTime; + TaskFinishTime = task.taskFinishTime; + IsLastTask = task.IsLastTask; + TimeLimit = task.TimeLimit; + } + + /// + /// Populates common task data fields to the specified object. + /// Should be called after the new task has been instantiated ToTask implementation of derived classes. + /// + /// The to populate. + protected void ToTaskCommon(Task task) + { + task.state = State; + task.taskStartTime = TaskStartTime; + task.taskFinishTime = TaskFinishTime; + task.isLastTask = IsLastTask; + task.TimeLimit = TimeLimit; + } + + /// + /// Serialises the common task data fields to the specified . + /// Should be called as the first step in the Serialize implementation of derived classes. + /// + /// The to write data to. + protected void SerializeCommon(BinaryWriter writer) + { + writer.Write(TaskNetId); + writer.Write((byte)State); + writer.Write(TaskStartTime); + writer.Write(TaskFinishTime); + writer.Write(IsLastTask); + writer.Write(TimeLimit); + writer.Write((byte)TaskType); + } + + + /// + /// Deserialises the common task data fields from the specified . + /// Should be called as the first step in the Deserialize implementation of derived classes. + /// + /// The to read data from. + protected void DeserializeCommon(BinaryReader reader) + { + TaskNetId = reader.ReadUInt16(); + State = (TaskState)reader.ReadByte(); + TaskStartTime = reader.ReadSingle(); + TaskFinishTime = reader.ReadSingle(); + IsLastTask = reader.ReadBoolean(); + TimeLimit = reader.ReadSingle(); + TaskType = (TaskType)reader.ReadByte(); + } +} + +#endregion diff --git a/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs b/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs new file mode 100644 index 00000000..fe8abdf8 --- /dev/null +++ b/MultiplayerAPI/Util/BinaryReaderWriterExtensions.cs @@ -0,0 +1,137 @@ +using System.IO; +using UnityEngine; + +namespace MPAPI.Util; + +/// +/// Provides extension methods for and to handle arrays and Unity types. +/// +public static class BinaryReaderWriterExtensions +{ + /// + /// Serialises a array. + /// + /// The to write to. + /// The array to write. If null, writes 0 as the length. + public static void WriteUShortArray(this BinaryWriter writer, ushort[] array) + { + if (array == null) + { + writer.Write(0); + return; + } + writer.Write(array.Length); + foreach (ushort value in array) + { + writer.Write(value); + } + } + + /// + /// Serialises an array. + /// + /// The to write to. + /// The array to serialise. If null, serialises 0 as the length. + public static void WriteInt32Array(this BinaryWriter writer, int[] array) + { + if (array == null) + { + writer.Write(0); + return; + } + writer.Write(array.Length); + foreach (int value in array) + { + writer.Write(value); + } + } + + /// + /// Serialises a . + /// + /// The to write to. + /// The to serialise. + public static void WriteVector3(this BinaryWriter writer, Vector3 vector) + { + writer.Write(vector.x); + writer.Write(vector.y); + writer.Write(vector.z); + } + + /// + /// Serialises a . + /// + /// The to write to. + /// The to serialise. + public static void WriteQuaternion(this BinaryWriter writer, Quaternion quaternion) + { + writer.Write(quaternion.w); + writer.Write(quaternion.x); + writer.Write(quaternion.y); + writer.Write(quaternion.z); + } + + /// + /// Deserialises a array. + /// + /// The to deserialise from. + /// The deserialised array. + public static ushort[] ReadUShortArray(this BinaryReader reader) + { + var length = reader.ReadInt32(); + + var ret = new ushort[length]; + for (int i = 0; i < length; i++) + { + ret[i] = reader.ReadUInt16(); + } + return ret; + } + + /// + /// Deserialises an array. + /// + /// The to deserialise from. + /// The deserialised array. + public static int[] ReadInt32Array(this BinaryReader reader) + { + var length = reader.ReadInt32(); + + var ret = new int[length]; + for (int i = 0; i < length; i++) + { + ret[i] = reader.ReadInt32(); + } + return ret; + } + + /// + /// Deserialises a . + /// + /// The to deserialise from. + /// The deserialised . + public static Vector3 ReadVector3(this BinaryReader reader) + { + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + + return new Vector3(x, y, z); + } + + /// + /// Deserialises a . + /// + /// The to deserialise from. + /// The deserialised . + + public static Quaternion ReadQuaternion(this BinaryReader reader) + { + float w = reader.ReadSingle(); + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + + return new Quaternion(x, y, z, w); + } +} diff --git a/MultiplayerAssets/Assets/AssetIndex.asset b/MultiplayerAssets/Assets/AssetIndex.asset index b1c4785e..48bf760e 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 5a667468..00000000 --- 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 6e1e67be..00000000 --- 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 7af13029..00000000 --- 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 eee7a723..00000000 --- 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 b70c4360..00000000 --- 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 2f5c4fac..00000000 --- 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 4a2cdd93..00000000 --- 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 811d8303..00000000 --- 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 13d8852e..00000000 --- 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 926caffc..00000000 --- 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 2eb09fe5..00000000 --- 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 a3dee0c0..00000000 --- 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 98e28a71..00000000 --- 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 3ee97d6c..00000000 --- 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 55a2fe8e..00000000 --- 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 b3d9b3a7..00000000 --- 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 5bfbd7d8..00000000 --- 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 9b671969..00000000 --- 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 db230749..00000000 --- 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 530c72ed..00000000 --- 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 bcaa3e64..00000000 --- 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 4a58e9ea..00000000 --- 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 30846960..00000000 --- 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 0032f3cf..00000000 --- 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 fa737432..00000000 --- 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 fc846c8b..00000000 --- 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 586308e3..00000000 --- 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 ca7dfbcd..00000000 --- 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 26bc130a..00000000 --- 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 44cb6f3e..00000000 --- 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 e74c9ba9..00000000 --- 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 08312209..00000000 --- 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 d5998d13..00000000 --- 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 aabeaa36..00000000 --- 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 ee9309f5..00000000 --- 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 f4599b48..00000000 --- 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 921a22cb..00000000 --- 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 4b403084..00000000 --- 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 05991c13..00000000 --- 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 9ae91ed8..00000000 --- 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 eee14064..00000000 --- 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 c369a0f0..00000000 --- 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 8407db64..00000000 --- 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 f7b2bd82..00000000 --- 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 a7878912..00000000 --- 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 dc92122a..00000000 --- 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 abdd2cf8..00000000 --- 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 26ef7bd8..00000000 --- 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 a7adb243..00000000 --- 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 4a10d170..00000000 --- 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 36ab889e..00000000 --- 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 bc47f86d..00000000 --- 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 6516eaac..00000000 --- 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 cbde7058..00000000 --- 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 7e85680c..00000000 --- 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 aa0417ee..00000000 --- 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 3ecd10c7..00000000 --- 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 71323b0b..00000000 --- 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 92f14bee..00000000 --- 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 52e88c1a..00000000 --- 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 6ddab0e5..00000000 --- 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 8354523b..00000000 --- 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 baa8351a..00000000 --- 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 212eeeeb..00000000 --- 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 c8e29bc0..00000000 --- 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 1a463ab0..00000000 --- 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 63f6cd67..00000000 --- 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 b75eed32..00000000 --- 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 1ba52107..00000000 --- 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 a47fed20..00000000 --- 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 bd7f74fe..00000000 --- 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 6f3e611c..00000000 --- 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 b73e1b90..00000000 --- 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 b2075b6a..00000000 --- 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 e5011459..00000000 --- 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 fc07e689..00000000 --- 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 dc0d8aa2..00000000 --- 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 72d771a2..00000000 --- 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 b0a87a0f..7f74bf27 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 f58bced5..7e505072 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/Scripts/Multiplayer/Exporter.cs b/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs index ebcff252..e259f357 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/Exporter.cs @@ -14,7 +14,6 @@ public static class Exporter private const string DEST_DIR = "../build"; private const string ASSET_BUNDLE_DEST_NAME = ASSET_BUNDLE_NAME + ".assetbundle"; private static readonly string[] COPY_DLLS = { - "LiteNetLib.dll", "MultiplayerEditor.dll", "UnityChan.dll" }; diff --git a/MultiplayerAssets/Assets/Textures/Connect.png b/MultiplayerAssets/Assets/Textures/Connect.png new file mode 100644 index 00000000..6b22b32a 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 00000000..30a876c5 --- /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 00000000..142abee2 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 00000000..2b294c14 --- /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 00000000..9f9062d8 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 00000000..570a6349 --- /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 00000000..dcb097ec 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 00000000..40ef2524 --- /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 b6bf4368..ed2953ed 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 b4953ac5..d948b24d 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 38fde5f3..d638f04d 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 457cd00b..efb6ac76 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 b6f7b0e6..e597f41e 100644 --- a/info.json +++ b/info.json @@ -1,9 +1,13 @@ { - "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.13.5", + "DisplayName": "Multiplayer", + "Author": "Insprill, Macka, Morm", + "EntryMethod": "Multiplayer.Multiplayer.Load", + "ManagerVersion": "0.27.3", + "LoadAfter": [ + "RemoteDispatch" + ], + "HomePage": "https://www.nexusmods.com/derailvalley/mods/1070", + "Repository": "https://raw.githubusercontent.com/AMacro/dv-multiplayer/refs/heads/beta/releases.json" } diff --git a/locale.csv b/locale.csv index f8b269b0..48b0b5a2 100644 --- a/locale.csv +++ b/locale.csv @@ -5,32 +5,120 @@ 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,,,,,,,,,,,,,,,,,,,,,,,,,, +mm/incompatible_mods,Message box for incompatible mods detected,Incompatible mods detected!\nPlease disable the following mods and reload the game:,,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_title,Message box title for Multiplayer mod update,Multiplayer Mod Update Available!,,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_latest,Message box title for Multiplayer mod update,Latest version:{0},,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_installed,Message box title for Multiplayer mod update,Installed version:{0},,,,,,,,,,,,,,,,,,,,,,,,, +mm/update_action,Message box title for Multiplayer mod update,Run Unity Mod Manager Installer to apply the update.,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,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/password_required_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/password_required_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/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/ok,'OK' for details text,OK,Добре,是,是,Ano,Ja,Ja,Kyllä,Oui,Ja,हाँ,Igen,Sì,はい,예,Ja,Tak,Sim,Sim,Da,Да,Áno,Sí,Ja,Evet,Так +sb/mismatch,'mismatch' for details text,Mismatch,Несъответствие,不匹配,不符,Nesoulad,Uoverensstemmelse,Niet-overeenkomend,Ristiriita,Non-concordance,Diskrepanz,बेमेल,Eltérés,Non corrispondenza,不一致,불일치,Avvik,Niezgodność,Incompatibilidade,Incompatibilidade,Nepotrivire,Несоответствие,Nesúlad,Discordancia,Oöverensstämmelse,Uyumsuzluk,Невідповідність +sb/missing,'Missing' for mod status,Missing,Липсва,缺少,缺少,Chybí,Mangler,Ontbreekt,Puuttuu,Manquant,Fehlt,गायब,Hiányzik,Mancante,欠落,누락,Mangler,Brak,Ausente,Ausente,Lipsă,Отсутствует,Chýba,Falta,Saknas,Eksik,Відсутній +sb/required_mods,'Required Mods' header,Required Mods,Необходими модификации,必需模组,必需模組,Požadované módy,Krævede mods,Vereiste mods,Vaaditut modit,Mods requis,Erforderliche Mods,आवश्यक मॉड,Szükséges modok,Mod richiesti,必須MOD,필수 모드,Nødvendige modifikasjoner,Wymagane mody,Mods necessários,Mods necessários,Moduri necesare,Требуемые моды,Požadované módy,Mods requeridos,Nödvändiga moddar,Gerekli modlar,Необхідні моди +sb/extra_mods,'Extra Mods' header,Extra Mods,Допълнителни модификации,多余模组,額外模組,Extra módy,Ekstra mods,Extra mods,Ylimääräiset modit,Mods supplémentaires,Zusätzliche Mods,अतिरिक्त मॉड,Extra modok,Mod extra,追加MOD,추가 모드,Ekstra modifikasjoner,Dodatkowe mody,Mods extras,Mods extras,Moduri suplimentare,Дополнительные моды,Extra módy,Mods adicionales,Extra moddar,Ekstra modlar,Додаткові моди +sb/incompatible,'Incompatible' for mod status,Incompatible,Несъвместим,不兼容,不相容,Nekompatibilní,Inkompatibel,Incompatibel,Yhteensopimaton,Incompatible,Inkompatibel,असंगत,Nem kompatibilis,Incompatibile,非互換,호환 불가,Inkompatibel,Niekompatybilny,Incompatível,Incompatível,Incompatibil,Несовместимо,Nekompatibilné,Incompatible,Inkompatibel,Uyumsuz,Несумісний +sb/extra_mod,'Extra Mod' status message,Extra Mod,Допълнителна модификация,多余模组,額外模組,Extra mód,Ekstra mod,Extra mod,Ylimääräinen modi,Mod supplémentaire,Zusätzlicher Mod,अतिरिक्त मॉड,Extra mod,Mod extra,追加MOD,추가 모드,Ekstra modifikasjon,Dodatkowy mod,Mod extra,Mod extra,Mod suplimentar,Дополнительный мод,Extra mód,Mod adicional,Extra modd,Ekstra mod,Додатковий мод +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/visibility,Server visibility selector label,Server Visibility,Видимост на сървъра,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility__tooltip,Server visibility selector tooltip,"Sets the visibility of the server. Private (Unlisted, Invite Only), Friends (Only Steam Friends or Invites), Public (Everyone)","Задава видимостта на сървъра. Частен (нерегистриран, само с покана), приятели (само приятели или покани в Steam), публичен (всички)",,,,,,,,,,,,,,,,,,,,,,,, +host/visibility__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility/modes/private,Possible visibiliy modes,Private,Частно,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility/modes/friends,Possible visibiliy modes,Friends,Приятели,,,,,,,,,,,,,,,,,,,,,,,, +host/visibility/modes/public,Possible visibiliy modes,Public,Публично,,,,,,,,,,,,,,,,,,,,,,,, +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,,,,,,,,,,,,,, +chat/whisper/not_found,Whisper error player not found 'player1 not found - you're whispering into the void!',{0} not found - you're whispering into the void!,,,,,,,,,,,,,,,,,,,,,,,,, +chat/kick/unable,Unable to kick player 'Unable to kick player1',Unable to kick {0},,,,,,,,,,,,,,,,,,,,,,,,, +chat/kick/kicked,Alert that a player has bee kicked 'player1 was kicked',{0} was kicked,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,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 474e0e57..00000000 --- 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 00000000..ae93e0be --- /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 diff --git a/releases.json b/releases.json new file mode 100644 index 00000000..24e70580 --- /dev/null +++ b/releases.json @@ -0,0 +1,6 @@ +{ + "Releases": + [ + {"Id": "Multiplayer", "Version": "0.1.13.5", "DownloadUrl": "https://github.com/AMacro/dv-multiplayer/releases/download/v0.1.13.5-Beta/Multiplayer.0.1.13.5.zip"} + ] +} \ No newline at end of file