diff --git a/.gitignore b/.gitignore index 416162b..636c71c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,68 +1,14 @@ +@@ -1,88 +1,1 @@ # ============ # # System Files # # ============ # .DS_Store ._* -# =============== # -# Unity generated # -# =============== # -[Aa]pp/ -[Aa]pp.meta -[Bb]in/ -[Bb]uilds/ -[Bb]uild/ -[Ll]ibrary/ -[Ll]ogs/ -[Oo]bj/ -[Tt]emp/ -UserSettings/ -UWP/ -WindowsStoreApp/ -UnityGenerated/ -UnityPackageManager/ -.out/ -.gradle/ -project.json -project.lock.json -*.package -TextMesh Pro.meta -TextMesh Pro/ -UIElementsSchema/ -*packages-lock.json - -# ============ # -# Certificates # -# ============ # -*.cert -*.privkey -*.pfx -*.pfx.meta - # ===================================== # # Visual Studio / MonoDevelop generated # # ===================================== # .vs/ -ExportedObj/ -obj/ -*.svd -*.userprefs -/*.csproj -*.csproj -*.pidb -*.suo -/*.sln -*.sln -*.user -*.unityproj -*.ipch -*.opensdf -*.sdf -*.tlog -*.log -*.idb -*.opendb -*.vsconfig # ============================ # # Visual Studio Code Generated # @@ -74,15 +20,3 @@ obj/ # ========================= # .idea/ _ReSharper.Caches - -# ===================== # -# Project Specific List # -# ===================== # ---Version/ -artifacts/ -StreamingAssets/ -StreamingAssets.meta - -# ====================== # -# Project Specific Links # -# ====================== # diff --git a/DemoWebsocketServer/.gitignore b/DemoWebsocketServer/.gitignore new file mode 100644 index 0000000..039a2e2 --- /dev/null +++ b/DemoWebsocketServer/.gitignore @@ -0,0 +1,399 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +oai-proxy-test-project-firebase-adminsdk-fkqi9-139d4b6ca2.json diff --git a/DemoWebsocketServer/DemoWebsocketServer.csproj b/DemoWebsocketServer/DemoWebsocketServer.csproj new file mode 100644 index 0000000..1b28a01 --- /dev/null +++ b/DemoWebsocketServer/DemoWebsocketServer.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/DemoWebsocketServer/DemoWebsocketServer.sln b/DemoWebsocketServer/DemoWebsocketServer.sln new file mode 100644 index 0000000..04669a3 --- /dev/null +++ b/DemoWebsocketServer/DemoWebsocketServer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34728.123 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoWebsocketServer", "DemoWebsocketServer.csproj", "{1DE6696C-53E1-4F77-8F18-6C7211862914}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1DE6696C-53E1-4F77-8F18-6C7211862914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1DE6696C-53E1-4F77-8F18-6C7211862914}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DE6696C-53E1-4F77-8F18-6C7211862914}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1DE6696C-53E1-4F77-8F18-6C7211862914}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {43621CB5-085C-4169-B2A8-C747180206A4} + EndGlobalSection +EndGlobal diff --git a/DemoWebsocketServer/Program.cs b/DemoWebsocketServer/Program.cs new file mode 100644 index 0000000..c7ff498 --- /dev/null +++ b/DemoWebsocketServer/Program.cs @@ -0,0 +1,91 @@ +using System.Net.WebSockets; +using System.Security.Cryptography; +using System.Text; +using System.Timers; + +internal class Program +{ + private static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + app.UseWebSockets(); + app.MapGet("/", RequestHandler); + app.Run(); + } + + private static async Task RequestHandler(HttpContext context) + { + try + { + if (context.WebSockets.IsWebSocketRequest) + { + using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + Console.WriteLine($"[{DateTime.UtcNow}] Websocket Connection Established!"); + + using var textTimer = new System.Timers.Timer(100); + + async void OnTextTimerOnElapsed(object? sender, ElapsedEventArgs e) + { + await webSocket.SendAsync(new ArraySegment("Hello World!"u8.ToArray()), WebSocketMessageType.Text, true, CancellationToken.None); + } + + textTimer.Elapsed += OnTextTimerOnElapsed; + textTimer.Start(); + + using var binaryTimer = new System.Timers.Timer(110); + + async void OnBinaryTimerOnElapsed(object? sender, ElapsedEventArgs e) + { + await webSocket.SendAsync(new ArraySegment(RandomNumberGenerator.GetBytes(8)), WebSocketMessageType.Binary, true, CancellationToken.None); + } + + binaryTimer.Elapsed += OnBinaryTimerOnElapsed; + binaryTimer.Start(); + + var buffer = new byte[1024 * 4]; + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + switch (result.MessageType) + { + case WebSocketMessageType.Text: + Console.WriteLine($"[{DateTime.UtcNow}] '{Encoding.UTF8.GetString(buffer, 0, result.Count)}'"); + break; + case WebSocketMessageType.Binary: + Console.WriteLine($"[{DateTime.UtcNow}] {BitConverter.ToString(buffer, 0, result.Count).Replace("-", ", ")}"); + break; + case WebSocketMessageType.Close: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + binaryTimer.Stop(); + binaryTimer.Elapsed -= OnBinaryTimerOnElapsed; + + textTimer.Stop(); + textTimer.Elapsed -= OnTextTimerOnElapsed; + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + Console.WriteLine($"[{DateTime.UtcNow}] Websocket Connection Closed!"); + } + else + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("Hello World!"); + } + } + catch (Exception e) + { + Console.WriteLine(e); + context.Response.StatusCode = 500; + await context.Response.WriteAsJsonAsync(e); + } + } +} \ No newline at end of file diff --git a/DemoWebsocketServer/Properties/launchSettings.json b/DemoWebsocketServer/Properties/launchSettings.json new file mode 100644 index 0000000..2c7ef9c --- /dev/null +++ b/DemoWebsocketServer/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:12713", + "sslPort": 44362 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5053", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7068;http://localhost:5053", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/DemoWebsocketServer/appsettings.Development.json b/DemoWebsocketServer/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/DemoWebsocketServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/DemoWebsocketServer/appsettings.json b/DemoWebsocketServer/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/DemoWebsocketServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/InitializeTemplate.ps1 b/InitializeTemplate.ps1 deleted file mode 100644 index d8772c9..0000000 --- a/InitializeTemplate.ps1 +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright (c) Stephen Hodgson. All rights reserved. -# Licensed under the MIT License. See LICENSE in the project root for license information. - -$InputAuthor = Read-Host "Set Author name: (i.e. your GitHub username)" -$ProjectAuthor = "ProjectAuthor" - -$InputName = Read-Host "Enter a name for your new project" -$ProjectName = "ProjectName" - -$InputScope = Read-Host "Enter a scope for your new project (optional)" - -if(-not [String]::IsNullOrWhiteSpace($InputScope)) { - $InputScope = "$InputScope." -} - -$ProjectScope = "ProjectScope." - -Write-Host "Your new com.$($InputScope.ToLower())$($InputName.ToLower()) project is being created..." -Remove-Item -Path ".\Readme.md" -Copy-Item -Path ".\$ProjectScope$ProjectName\Packages\com.$($ProjectScope.ToLower())$($ProjectName.ToLower())\Documentation~\Readme.md" ` - -Destination ".\Readme.md" - -# Rename any directories before we crawl the folders -Rename-Item -Path ".\$ProjectScope$ProjectName\Packages\com.$($ProjectScope.ToLower())$($ProjectName.ToLower())\Runtime\$ProjectScope$ProjectName.asmdef" ` - -NewName "$InputScope$InputName.asmdef" -Rename-Item -Path ".\$ProjectScope$ProjectName\Packages\com.$($ProjectScope.ToLower())$($ProjectName.ToLower())\Editor\$ProjectScope$ProjectName.Editor.asmdef" ` - -NewName "$InputScope$InputName.Editor.asmdef" -Rename-Item -Path ".\$ProjectScope$ProjectName\Packages\com.$($ProjectScope.ToLower())$($ProjectName.ToLower())\Tests\$ProjectScope$ProjectName.Tests.asmdef" ` - -NewName "$InputScope$InputName.Tests.asmdef" -Rename-Item -Path ".\$ProjectScope$ProjectName\Packages\com.$($ProjectScope.ToLower())$($ProjectName.ToLower())" ` - -NewName "com.$($InputScope.ToLower())$($InputName.ToLower())" -Rename-Item -Path ".\$ProjectScope$ProjectName" ` - -NewName ".\$InputScope$InputName" - -$excludes = @('*Library*', '*Obj*','*InitializeTemplate*') -Get-ChildItem -Path "*"-File -Recurse -Exclude $excludes | ForEach-Object -Process { - $isValid = $true - - foreach ($exclude in $excludes) { - if ((Split-Path -Path $_.FullName -Parent) -ilike $exclude) { - $isValid = $false - break - } - } - - if ($isValid) { - Get-ChildItem -Path $_ -File | ForEach-Object -Process { - $updated = $false; - - $fileContent = Get-Content $($_.FullName) -Raw - - # Rename all PascalCase instances - if ($fileContent -cmatch $ProjectName) { - $fileContent -creplace $ProjectName, $InputName | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - if ($fileContent -cmatch $ProjectScope) { - $fileContent -creplace $ProjectScope, $InputScope | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - if ($fileContent -cmatch $ProjectAuthor) { - $fileContent -creplace $ProjectAuthor, $InputAuthor | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - $StephenHodgson = "StephenHodgson" - - if ($fileContent -cmatch $StephenHodgson) { - $fileContent -creplace $StephenHodgson, $InputAuthor | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - # Rename all lowercase instances - if ($fileContent -cmatch $ProjectName.ToLower()) { - $fileContent -creplace $ProjectName.ToLower(), $InputName.ToLower() | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - if ($fileContent -cmatch $ProjectScope.ToLower()) { - $fileContent -creplace $ProjectScope.ToLower(), $InputScope.ToLower() | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - # Rename all UPPERCASE instances - if ($fileContent -cmatch $ProjectName.ToUpper()) { - $fileContent -creplace $ProjectName.ToUpper(), $InputName.ToUpper() | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - if ($fileContent -cmatch $ProjectScope.ToUpper()) { - $fileContent -creplace $ProjectScope.ToUpper(), $InputScope.ToUpper() | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - # Update guids - if ($fileContent -match "#INSERT_GUID_HERE#") { - $fileContent -replace "#INSERT_GUID_HERE#", [guid]::NewGuid() | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - $fileContent = Get-Content $($_.FullName) -Raw - - # Update year - if ($fileContent -match "#CURRENT_YEAR#") { - $fileContent -replace "#CURRENT_YEAR#", (Get-Date).year | Set-Content $($_.FullName) -NoNewline - $updated = $true - } - - # Rename files - if ($_.Name -match $ProjectName) { - Rename-Item -LiteralPath $_.FullName -NewName ($_.Name -replace ($ProjectName, $InputName)) - $updated = $true - } - - if ($updated) { - Write-Host $_.Name - } - } - } -} - -Remove-Item -Path "InitializeTemplate.ps1" diff --git a/LICENSE.md b/LICENSE.md index 874421a..ae10890 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) #CURRENT_YEAR# ProjectAuthor +Copyright (c) 2024 RageAgainstThePixel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Documentation~/README.md b/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Documentation~/README.md deleted file mode 100644 index f4c1cf8..0000000 --- a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Documentation~/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# com.projectscope.projectname - -[![Discord](https://img.shields.io/discord/855294214065487932.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/xQgMW9ufN4) [![openupm](https://img.shields.io/npm/v/com.projectscope.projectname?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.projectscope.projectname/) [![openupm](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=downloads&query=%24.downloads&suffix=%2Fmonth&url=https%3A%2F%2Fpackage.openupm.com%2Fdownloads%2Fpoint%2Flast-month%2Fcom.projectscope.projectname)](https://openupm.com/packages/com.projectscope.projectname/) - -A ProjectScope.ProjectName package for the [Unity](https://unity.com/) Game Engine. - -## Installing - -Requires Unity 2021.3 LTS or higher. - -The recommended installation method is though the unity package manager and [OpenUPM](https://openupm.com/packages/com.projectscope.projectname). - -### Via Unity Package Manager and OpenUPM - -- Open your Unity project settings -- Select the `Package Manager` -![scoped-registries](images/package-manager-scopes.png) -- Add the OpenUPM package registry: - - Name: `OpenUPM` - - URL: `https://package.openupm.com` - - Scope(s): - - `com.projectscope.projectname` -- Open the Unity Package Manager window -- Change the Registry from Unity to `My Registries` -- Add the `ProjectScope.ProjectName` package - -### Via Unity Package Manager and Git url - -- Open your Unity Package Manager -- Add package from git url: `https://github.com/ProjectAuthor/com.projectscope.projectname.git#upm` - -## Documentation - -### Project Setup - -```csharp -// TODO -``` diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Tests/ExampleTestScript.cs b/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Tests/ExampleTestScript.cs deleted file mode 100644 index 9fcb46b..0000000 --- a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Tests/ExampleTestScript.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed under the MIT License. See LICENSE in the project root for license information. - -using NUnit.Framework; - -namespace ProjectScope.ProjectName.Tests -{ - internal class ExampleTestScript - { - [Test] - public void ExampleTestScriptSimplePasses() - { - // A Test behaves as an ordinary method - // Use the Assert class to test conditions - } - } -} diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/package.json b/ProjectScope.ProjectName/Packages/com.projectscope.projectname/package.json deleted file mode 100644 index af591d9..0000000 --- a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "com.projectscope.projectname", - "displayName": "ProjectScope.ProjectName", - "description": "ProjectScope.ProjectName description goes here.", - "keywords": [], - "version": "1.0.0-preview.1", - "unity": "2021.3", - "documentationUrl": "https://github.com/ProjectAuthor/com.projectscope.projectname#documentation", - "changelogUrl": "https://github.com/ProjectAuthor/com.projectscope.projectname/releases", - "license": "MIT", - "repository": { - "type": "git", - "repository": "https://github.com/ProjectAuthor/com.projectscope.projectname.git" - }, - "author": { - "name": "ProjectAuthor", - "url": "https://github.com/ProjectAuthor" - }, - "dependencies": {}, - "publishConfig": { - "registry": "https://package.openupm.com" - } -} diff --git a/README.md b/README.md index 81adf07..b93484e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,149 @@ -# upm-template +# com.utilities.websockets -A Unity package manager repository template for quickly creating and setting up new UPM package projects in Unity. +[![Discord](https://img.shields.io/discord/855294214065487932.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/xQgMW9ufN4) [![openupm](https://img.shields.io/npm/v/com.utilities.websockets?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.utilities.websockets/) [![openupm](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=downloads&query=%24.downloads&suffix=%2Fmonth&url=https%3A%2F%2Fpackage.openupm.com%2Fdownloads%2Fpoint%2Flast-month%2Fcom.utilities.websockets)](https://openupm.com/packages/com.utilities.websockets/) -## Getting Started +A simple websocket package for the [Unity](https://unity.com/) Game Engine. -1. Create a new repository in GitHub using this template -2. Clone to your local machine -3. Run the `InitializeTemplate.ps1` script from powershell, a command line, or terminal -4. Follow prompt instructions -5. Open in Unity to generate the missing meta files -6. Check in project to source control +## Installing + +Requires Unity 2021.3 LTS or higher. + +The recommended installation method is though the unity package manager and [OpenUPM](https://openupm.com/packages/com.utilities.websockets). + +### Via Unity Package Manager and OpenUPM + +- Open your Unity project settings +- Select the `Package Manager` +![scoped-registries](Utilities.Websockets/Packages/com.utilities.websockets/Documentation~/images/package-manager-scopes.png) +- Add the OpenUPM package registry: + - Name: `OpenUPM` + - URL: `https://package.openupm.com` + - Scope(s): + - `com.utilities` +- Open the Unity Package Manager window +- Change the Registry from Unity to `My Registries` +- Add the `Utilities.Websockets` package + +### Via Unity Package Manager and Git url + +- Open your Unity Package Manager +- Add package from git url: `https://github.com/RageAgainstThePixel/com.utilities.websockets.git#upm` + > Note: this repo has dependencies on other repositories! You are responsible for adding these on your own. + - [com.utilities.async](https://github.com/RageAgainstThePixel/com.utilities.async) + +--- + +## Documentation + +### Table Of Contents + +- [Connect to a Server](#connect-to-a-server) +- [Handling Events](#handling-events) + - [OnOpen](#onopen) + - [OnMessage](#onmessage) + - [OnError](#onerror) + - [OnClose](#onclose) +- [Sending Messages](#sending-messages) + - [Text](#sending-text) + - [Binary](#sending-binary) +- [Disconnect from a Server](#disconnect-from-a-server) + +### Connect to a Server + +To setup a new connection, create a new instance of WebSocket and subscribe to event callbacks, and call `Connect` or `ConnectAsync` methods. + +> Note: WebSocket implements `IDisposable` and should be properly disposed after use! + +```csharp +var address = "wss://echo.websocket.events"; +using var socket = new WebSocket(address); +socket.OnOpen += () => Debug.Log($"Connection Established @ {address}"); +socket.OnMessage += (dataFrame) => { + switch (dataFrame.Type) + { + case OpCode.Text: + AddLog($"<- Received: {dataFrame.Text}"); + break; + case OpCode.Binary: + AddLog($"<- Received: {dataFrame.Data.Length} Bytes"); + break; + } +}; +socket.OnError += (exception) => Debug.LogException(exception); +socket.OnClose += (code, reason) => Debug.Log($"Connection Closed: {code} {reason}"); +socket.Connect(); +``` + +### Handling Events + +You can subscribe to the `OnOpen`, `OnMessage`, `OnError`, and `OnClose` events to handle respective situations: + +#### OnOpen + +Event triggered when the WebSocket connection has been established. + +```csharp +socket.OnOpen += () => Debug.Log("Connection Established!"); +``` + +#### OnMessage + +Event triggered when the WebSocket receives a message. The callback contains a data frame, which can be either text or binary. + +```csharp +socket.OnMessage += (dataFrame) => { + switch (dataFrame.Type) + { + case OpCode.Text: + AddLog($"<- Received: {dataFrame.Text}"); + break; + case OpCode.Binary: + AddLog($"<- Received: {dataFrame.Data.Length} Bytes"); + break; + } +}; +``` + +#### OnError + +Event triggered when the WebSocket raises an error. The callback contains an exception which can be handled, re-thrown, or logged. + +```csharp +socket.OnError += (exception) => Debug.LogException(exception); +``` + +#### OnClose + +Event triggered when the WebSocket connection has been closed. The callback contains the close code and reason. + +```csharp +socket.OnClose += (code, reason) => Debug.Log($"Connection Closed: {code} {reason}"); +``` + +### Sending Messages + +#### Sending Text + +Perfect for sending json payloads and other text messages. + +```csharp +await socket.SendAsync("{\"message\":\"Hello World!\"}"); +``` + +#### Sending Binary + +Perfect for sending binary data and files. + +```csharp +var bytes = System.Text.Encoding.UTF8.GetBytes("Hello World!"); +await socket.SendAsync(bytes); +``` + +### Disconnect from a Server + +To disconnect from the server, use `Close` or `CloseAsync` methods and dispose of the WebSocket. + +```csharp +socket.Close(); +socket.Dispose(); +``` diff --git a/ProjectScope.ProjectName/.editorconfig b/Utilities.Websockets/.editorconfig similarity index 100% rename from ProjectScope.ProjectName/.editorconfig rename to Utilities.Websockets/.editorconfig diff --git a/Utilities.Websockets/.gitignore b/Utilities.Websockets/.gitignore new file mode 100644 index 0000000..416162b --- /dev/null +++ b/Utilities.Websockets/.gitignore @@ -0,0 +1,88 @@ +# ============ # +# System Files # +# ============ # +.DS_Store +._* + +# =============== # +# Unity generated # +# =============== # +[Aa]pp/ +[Aa]pp.meta +[Bb]in/ +[Bb]uilds/ +[Bb]uild/ +[Ll]ibrary/ +[Ll]ogs/ +[Oo]bj/ +[Tt]emp/ +UserSettings/ +UWP/ +WindowsStoreApp/ +UnityGenerated/ +UnityPackageManager/ +.out/ +.gradle/ +project.json +project.lock.json +*.package +TextMesh Pro.meta +TextMesh Pro/ +UIElementsSchema/ +*packages-lock.json + +# ============ # +# Certificates # +# ============ # +*.cert +*.privkey +*.pfx +*.pfx.meta + +# ===================================== # +# Visual Studio / MonoDevelop generated # +# ===================================== # +.vs/ +ExportedObj/ +obj/ +*.svd +*.userprefs +/*.csproj +*.csproj +*.pidb +*.suo +/*.sln +*.sln +*.user +*.unityproj +*.ipch +*.opensdf +*.sdf +*.tlog +*.log +*.idb +*.opendb +*.vsconfig + +# ============================ # +# Visual Studio Code Generated # +# ============================ # +.vscode/ + +# ========================= # +# Jetbrains Rider Generated # +# ========================= # +.idea/ +_ReSharper.Caches + +# ===================== # +# Project Specific List # +# ===================== # +--Version/ +artifacts/ +StreamingAssets/ +StreamingAssets.meta + +# ====================== # +# Project Specific Links # +# ====================== # diff --git a/ProjectScope.ProjectName/Assets/csc.rsp b/Utilities.Websockets/Assets/csc.rsp similarity index 100% rename from ProjectScope.ProjectName/Assets/csc.rsp rename to Utilities.Websockets/Assets/csc.rsp diff --git a/Utilities.Websockets/Assets/csc.rsp.meta b/Utilities.Websockets/Assets/csc.rsp.meta new file mode 100644 index 0000000..009f4d9 --- /dev/null +++ b/Utilities.Websockets/Assets/csc.rsp.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7cedafe8db244e94a998b0fa26433118 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Documentation~/README.md b/Utilities.Websockets/Packages/com.utilities.websockets/Documentation~/README.md new file mode 100644 index 0000000..30c1854 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Documentation~/README.md @@ -0,0 +1,149 @@ +# com.utilities.websockets + +[![Discord](https://img.shields.io/discord/855294214065487932.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/xQgMW9ufN4) [![openupm](https://img.shields.io/npm/v/com.utilities.websockets?label=openupm®istry_uri=https://package.openupm.com)](https://openupm.com/packages/com.utilities.websockets/) [![openupm](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=downloads&query=%24.downloads&suffix=%2Fmonth&url=https%3A%2F%2Fpackage.openupm.com%2Fdownloads%2Fpoint%2Flast-month%2Fcom.utilities.websockets)](https://openupm.com/packages/com.utilities.websockets/) + +A simple websocket package for the [Unity](https://unity.com/) Game Engine. + +## Installing + +Requires Unity 2021.3 LTS or higher. + +The recommended installation method is though the unity package manager and [OpenUPM](https://openupm.com/packages/com.utilities.websockets). + +### Via Unity Package Manager and OpenUPM + +- Open your Unity project settings +- Select the `Package Manager` +![scoped-registries](images/package-manager-scopes.png) +- Add the OpenUPM package registry: + - Name: `OpenUPM` + - URL: `https://package.openupm.com` + - Scope(s): + - `com.utilities` +- Open the Unity Package Manager window +- Change the Registry from Unity to `My Registries` +- Add the `Utilities.Websockets` package + +### Via Unity Package Manager and Git url + +- Open your Unity Package Manager +- Add package from git url: `https://github.com/RageAgainstThePixel/com.utilities.websockets.git#upm` + > Note: this repo has dependencies on other repositories! You are responsible for adding these on your own. + - [com.utilities.async](https://github.com/RageAgainstThePixel/com.utilities.async) + +--- + +## Documentation + +### Table Of Contents + +- [Connect to a Server](#connect-to-a-server) +- [Handling Events](#handling-events) + - [OnOpen](#onopen) + - [OnMessage](#onmessage) + - [OnError](#onerror) + - [OnClose](#onclose) +- [Sending Messages](#sending-messages) + - [Text](#sending-text) + - [Binary](#sending-binary) +- [Disconnect from a Server](#disconnect-from-a-server) + +### Connect to a Server + +To setup a new connection, create a new instance of WebSocket and subscribe to event callbacks, and call `Connect` or `ConnectAsync` methods. + +> Note: WebSocket implements `IDisposable` and should be properly disposed after use! + +```csharp +var address = "wss://echo.websocket.events"; +using var socket = new WebSocket(address); +socket.OnOpen += () => Debug.Log($"Connection Established @ {address}"); +socket.OnMessage += (dataFrame) => { + switch (dataFrame.Type) + { + case OpCode.Text: + AddLog($"<- Received: {dataFrame.Text}"); + break; + case OpCode.Binary: + AddLog($"<- Received: {dataFrame.Data.Length} Bytes"); + break; + } +}; +socket.OnError += (exception) => Debug.LogException(exception); +socket.OnClose += (code, reason) => Debug.Log($"Connection Closed: {code} {reason}"); +socket.Connect(); +``` + +### Handling Events + +You can subscribe to the `OnOpen`, `OnMessage`, `OnError`, and `OnClose` events to handle respective situations: + +#### OnOpen + +Event triggered when the WebSocket connection has been established. + +```csharp +socket.OnOpen += () => Debug.Log("Connection Established!"); +``` + +#### OnMessage + +Event triggered when the WebSocket receives a message. The callback contains a data frame, which can be either text or binary. + +```csharp +socket.OnMessage += (dataFrame) => { + switch (dataFrame.Type) + { + case OpCode.Text: + AddLog($"<- Received: {dataFrame.Text}"); + break; + case OpCode.Binary: + AddLog($"<- Received: {dataFrame.Data.Length} Bytes"); + break; + } +}; +``` + +#### OnError + +Event triggered when the WebSocket raises an error. The callback contains an exception which can be handled, re-thrown, or logged. + +```csharp +socket.OnError += (exception) => Debug.LogException(exception); +``` + +#### OnClose + +Event triggered when the WebSocket connection has been closed. The callback contains the close code and reason. + +```csharp +socket.OnClose += (code, reason) => Debug.Log($"Connection Closed: {code} {reason}"); +``` + +### Sending Messages + +#### Sending Text + +Perfect for sending json payloads and other text messages. + +```csharp +await socket.SendAsync("{\"message\":\"Hello World!\"}"); +``` + +#### Sending Binary + +Perfect for sending binary data and files. + +```csharp +var bytes = System.Text.Encoding.UTF8.GetBytes("Hello World!"); +await socket.SendAsync(bytes); +``` + +### Disconnect from a Server + +To disconnect from the server, use `Close` or `CloseAsync` methods and dispose of the WebSocket. + +```csharp +socket.Close(); +socket.Dispose(); +``` diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Documentation~/images/package-manager-scopes.png b/Utilities.Websockets/Packages/com.utilities.websockets/Documentation~/images/package-manager-scopes.png similarity index 100% rename from ProjectScope.ProjectName/Packages/com.projectscope.projectname/Documentation~/images/package-manager-scopes.png rename to Utilities.Websockets/Packages/com.utilities.websockets/Documentation~/images/package-manager-scopes.png diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Editor.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Editor.meta new file mode 100644 index 0000000..bb383ac --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 829a2a29ce04aed4aa31a5d54660c44c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Editor/AssemblyInfo.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/AssemblyInfo.cs similarity index 100% rename from ProjectScope.ProjectName/Packages/com.projectscope.projectname/Editor/AssemblyInfo.cs rename to Utilities.Websockets/Packages/com.utilities.websockets/Editor/AssemblyInfo.cs diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Editor/AssemblyInfo.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/AssemblyInfo.cs.meta new file mode 100644 index 0000000..d12d33f --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 20b20f831b318e5459f672cb68f03cf5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Editor/ProjectScope.ProjectName.Editor.asmdef b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/Utilities.Websockets.Editor.asmdef similarity index 69% rename from ProjectScope.ProjectName/Packages/com.projectscope.projectname/Editor/ProjectScope.ProjectName.Editor.asmdef rename to Utilities.Websockets/Packages/com.utilities.websockets/Editor/Utilities.Websockets.Editor.asmdef index ff9b17a..0cde049 100644 --- a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Editor/ProjectScope.ProjectName.Editor.asmdef +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/Utilities.Websockets.Editor.asmdef @@ -1,8 +1,8 @@ { - "name": "ProjectScope.ProjectName.Editor", - "rootNamespace": "ProjectScope.ProjectName.Editor", + "name": "Utilities.WebSockets.Editor", + "rootNamespace": "Utilities.WebSockets.Editor", "references": [ - "ProjectScope.ProjectName" + "Utilities.WebSockets" ], "includePlatforms": [ "Editor" diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Editor/Utilities.Websockets.Editor.asmdef.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/Utilities.Websockets.Editor.asmdef.meta new file mode 100644 index 0000000..a05b158 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Editor/Utilities.Websockets.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: edb38984470ed7b42b651ccb73c1ff02 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/LICENSE.md b/Utilities.Websockets/Packages/com.utilities.websockets/LICENSE.md similarity index 96% rename from ProjectScope.ProjectName/Packages/com.projectscope.projectname/LICENSE.md rename to Utilities.Websockets/Packages/com.utilities.websockets/LICENSE.md index 874421a..ae10890 100644 --- a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/LICENSE.md +++ b/Utilities.Websockets/Packages/com.utilities.websockets/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) #CURRENT_YEAR# ProjectAuthor +Copyright (c) 2024 RageAgainstThePixel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/LICENSE.md.meta b/Utilities.Websockets/Packages/com.utilities.websockets/LICENSE.md.meta new file mode 100644 index 0000000..24bb1a1 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/LICENSE.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1873865bc54a29a4fb38b402c58b00a2 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime.meta new file mode 100644 index 0000000..c880f55 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 5fcb3c3f74f4fdc47bc5ffec866b8f25 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Runtime/AssemblyInfo.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/AssemblyInfo.cs similarity index 100% rename from ProjectScope.ProjectName/Packages/com.projectscope.projectname/Runtime/AssemblyInfo.cs rename to Utilities.Websockets/Packages/com.utilities.websockets/Runtime/AssemblyInfo.cs diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/AssemblyInfo.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/AssemblyInfo.cs.meta new file mode 100644 index 0000000..a3b791b --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1916059d33c5f6246b3e05e32a48a1ed +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/CloseStatusCode.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/CloseStatusCode.cs new file mode 100644 index 0000000..51035ff --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/CloseStatusCode.cs @@ -0,0 +1,80 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Utilities.WebSockets +{ + /// + /// When closing an established connection (e.g., when sending a Close frame, after the opening handshake has completed), + /// an endpoint MAY indicate a reason for closure. + /// + /// + /// The values of this enumeration are defined in . + /// + public enum CloseStatusCode : ushort + { + /// + /// Indicates a normal closure, meaning that the purpose for which the connection was established has been fulfilled. + /// + Normal = 1000, + /// + /// Indicates that an endpoint is "going away", such as a server going down or a browser having navigated away from a page. + /// + GoingAway = 1001, + /// + /// Indicates that an endpoint is terminating the connection due to a protocol error. + /// + ProtocolError = 1002, + /// + /// Indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept + /// (e.g., an endpoint that understands only text data MAY send this if it receives a binary message). + /// + UnsupportedData = 1003, + /// + /// Reserved and MUST NOT be set as a status code in a Close control frame by an endpoint. + /// The specific meaning might be defined in the future. + /// + Reserved = 1004, + /// + /// Reserved and MUST NOT be set as a status code in a Close control frame by an endpoint. + /// It is designated for use in applications expecting a status code to indicate that no status code was actually present. + /// + NoStatus = 1005, + /// + /// Reserved and MUST NOT be set as a status code in a Close control frame by an endpoint. + /// It is designated for use in applications expecting a status code to indicate that the connection was closed abnormally, + /// e.g., without sending or receiving a Close control frame. + /// + AbnormalClosure = 1006, + /// + /// Indicates that an endpoint is terminating the connection because it has received data within a message + /// that was not consistent with the type of the message. + /// + InvalidPayloadData = 1007, + /// + /// Indicates that an endpoint is terminating the connection because it received a message that violates its policy. + /// This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) + /// or if there is a need to hide specific details about the policy. + /// + PolicyViolation = 1008, + /// + /// Indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process. + /// + TooBigToProcess = 1009, + /// + /// Indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate + /// one or more extension, but the server didn't return them in the response message of the WebSocket handshake. + /// The list of extensions that are needed SHOULD appear in the /reason/ part of the Close frame. Note that this status code + /// is not used by the server, because it can fail the WebSocket handshake instead. + /// + MandatoryExtension = 1010, + /// + /// Indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request. + /// + ServerError = 1011, + /// + /// Reserved and MUST NOT be set as a status code in a Close control frame by an endpoint. + /// It is designated for use in applications expecting a status code to indicate that the connection was closed due to a failure to perform a TLS handshake + /// (e.g., the server certificate can't be verified). + /// + TlsHandshakeFailure = 1015 + } +} diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/CloseStatusCode.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/CloseStatusCode.cs.meta new file mode 100644 index 0000000..a00f26c --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/CloseStatusCode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9b749be3e094b1b48b5233eba7710b23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/DataFrame.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/DataFrame.cs new file mode 100644 index 0000000..14ec72b --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/DataFrame.cs @@ -0,0 +1,24 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +namespace Utilities.WebSockets +{ + public class DataFrame + { + public OpCode Type { get; } + + public ReadOnlyMemory Data { get; } + + public string Text { get; } + + public DataFrame(OpCode type, ReadOnlyMemory data) + { + Type = type; + Data = data; + Text = type == OpCode.Text + ? System.Text.Encoding.UTF8.GetString(data.Span) + : string.Empty; + } + } +} diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/DataFrame.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/DataFrame.cs.meta new file mode 100644 index 0000000..872672f --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/DataFrame.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 196ef085c1e622d4992be519a21fad3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/IWebSocket.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/IWebSocket.cs new file mode 100644 index 0000000..ccb7dd3 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/IWebSocket.cs @@ -0,0 +1,85 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Utilities.WebSockets +{ + public interface IWebSocket : IDisposable + { + /// + /// Occurs when the connection has been established. + /// + event Action OnOpen; + + /// + /// Occurs when the receives a message. + /// + event Action OnMessage; + + /// + /// Occurs when the raises an error. + /// + event Action OnError; + + /// + /// Occurs when the connection has been closed. + /// + event Action OnClose; + + /// + /// The address of the . + /// + Uri Address { get; } + + /// + /// The sub-protocols used by the . + /// + IReadOnlyList SubProtocols { get; } + + /// + /// The current state of the . + /// + State State { get; } + + /// + /// Connect to the server. + /// + void Connect(); + + /// + /// Connect to the server asynchronously. + /// + /// Optional, . + Task ConnectAsync(CancellationToken cancellationToken = default); + + /// + /// Send a text message to the . + /// + /// The text message to send. + /// Optional, . + Task SendAsync(string text, CancellationToken cancellationToken = default); + + /// + /// Send a binary message to the . + /// + /// The binary message to send. + /// Optional, . + Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default); + + /// + /// Close the . + /// + void Close(); + + /// + /// Close the asynchronously. + /// + /// The close status code. + /// The reason for closing the connection. + /// Optional, . + Task CloseAsync(CloseStatusCode code = CloseStatusCode.Normal, string reason = "", CancellationToken cancellationToken = default); + } +} diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/IWebSocket.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/IWebSocket.cs.meta new file mode 100644 index 0000000..aefe1f7 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/IWebSocket.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 251bd742d58dd8f48a56394e5586a74c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/OpCode.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/OpCode.cs new file mode 100644 index 0000000..a9f2ecd --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/OpCode.cs @@ -0,0 +1,10 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +namespace Utilities.WebSockets +{ + public enum OpCode + { + Text, + Binary + } +} diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/OpCode.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/OpCode.cs.meta new file mode 100644 index 0000000..21b21ed --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/OpCode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e0325f235b52ed04fb6eab828f85b3ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins.meta new file mode 100644 index 0000000..18508e5 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 150fe18e639eaf74aba90fe3723ddb84 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins/WebSocket.jslib b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins/WebSocket.jslib new file mode 100644 index 0000000..d1d39ce --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins/WebSocket.jslib @@ -0,0 +1,247 @@ +var UnityWebSocketLibrary = { + /** + * Pointer index for WebSocket objects. + */ + $ptrIndex: 0, + /** + * Array of instanced WebSocket objects. + */ + $webSockets: [], + /** + * Create a new WebSocket instance and adds it to the $webSockets array. + * @param {string} url - The URL to which to connect. + * @param {string[]} subProtocols - An array of strings that indicate the sub-protocols the client is willing to speak. + * @returns {number} - A pointer to the WebSocket instance. + * @param {function} onOpenCallback - The callback function. WebSocket_OnOpenDelegate(IntPtr websocketPtr) in C#. + * @param {function} onMessageCallback - The callback function. WebSocket_OnMessageDelegate(IntPtr websocketPtr, IntPtr data, int length, int type) in C#. + * @param {function} onErrorCallback - The callback function. WebSocket_OnErrorDelegate(IntPtr websocketPtr, IntPtr messagePtr) in C#. + * @param {function} onCloseCallback - The callback function. WebSocket_OnCloseDelegate(IntPtr websocketPtr, int code, IntPtr reasonPtr) in C#. + */ + WebSocket_Create: function (url, subProtocols, onOpenCallback, onMessageCallback, onErrorCallback, onCloseCallback) { + var urlStr = UTF8ToString(url); + + try { + var subProtocolsStr = UTF8ToString(subProtocols); + var subProtocolsArr = subProtocolsStr ? subProtocolsStr.split(',') : undefined; + + for (var i = 0; i < webSockets.length; i++) { + var instance = webSockets[i]; + + if (instance !== undefined && instance.url !== undefined && instance.url === urlStr) { + console.error('WebSocket connection already exists for URL: ', urlStr); + return 0; + } + } + + var socketPtr = ++ptrIndex; + webSockets[socketPtr] = { + socket: null, + url: urlStr, + onOpenCallback: onOpenCallback, + onMessageCallback: onMessageCallback, + onErrorCallback: onErrorCallback, + onCloseCallback: onCloseCallback + }; + + if (subProtocolsArr) { + webSockets[socketPtr].subProtocols = subProtocolsArr; + } + + // console.log('Created WebSocket object with websocketPtr: ', socketPtr, ' for URL: ', urlStr, ' and sub-protocols: ', subProtocolsArr) + return socketPtr; + } catch (error) { + console.error('Error creating WebSocket object for URL: ', urlStr, ' Error: ', error); + return 0; + } + }, + /** + * Get the current state of the WebSocket connection. + * @param socketPtr - A pointer to the WebSocket object. IntPtr in C#. + * @returns {number} - The current state of the WebSocket connection. + */ + WebSocket_GetState: function (socketPtr) { + try { + var instance = webSockets[socketPtr]; + + if (!instance || !instance.socket) { + return 0; + } + + return instance.socket.readyState; + } catch (error) { + console.error('Error getting WebSocket state for websocketPtr: ', socketPtr, ' Error: ', error); + return 3; + } + }, + /** + * Connect the WebSocket connection. + * @param socketPtr - A pointer to the WebSocket object. IntPtr in C#. + */ + WebSocket_Connect: function (socketPtr) { + try { + var instance = webSockets[socketPtr]; + + if (!instance.subProtocols || instance.subProtocols.length === 0) { + instance.socket = new WebSocket(instance.url); + } else { + instance.socket = new WebSocket(instance.url, instance.subProtocols); + } + + instance.socket.binaryType = 'arraybuffer'; + instance.socket.onopen = function () { + try { + // console.log('WebSocket connection opened for websocketPtr: ', socketPtr); + Module.dynCall_vi(instance.onOpenCallback, socketPtr); + } catch (error) { + console.error('Error calling onOpen callback for websocketPtr: ', socketPtr, ' Error: ', error); + } + }; + instance.socket.onmessage = function (event) { + try { + // console.log('Received message for websocketPtr: ', socketPtr, ' with data: ', event.data); + if (event.data instanceof ArrayBuffer) { + var array = new Uint8Array(event.data); + var buffer = Module._malloc(array.length); + writeArrayToMemory(array, buffer); + + try { + Module.dynCall_viiii(instance.onMessageCallback, socketPtr, buffer, array.length, 1); + } finally { + Module._free(buffer); + } + } else if (typeof event.data === 'string') { + var length = lengthBytesUTF8(event.data) + 1; + var buffer = Module._malloc(length); + stringToUTF8(event.data, buffer, length); + + try { + Module.dynCall_viiii(instance.onMessageCallback, socketPtr, buffer, length, 0); + } finally { + Module._free(buffer); + } + } else { + console.error('Error parsing message for websocketPtr: ', socketPtr, ' with data: ', event.data); + } + } catch (error) { + console.error('Error calling onMessage callback for websocketPtr: ', socketPtr, ' Error: ', error); + } + }; + instance.socket.onerror = function (event) { + try { + console.error('WebSocket error for websocketPtr: ', socketPtr, ' with message: ', event); + var json = JSON.stringify(event); + var length = lengthBytesUTF8(json) + 1; + var buffer = Module._malloc(length); + stringToUTF8(json, buffer, length); + + try { + Module.dynCall_vii(instance.onErrorCallback, socketPtr, buffer); + } finally { + Module._free(buffer); + } + } catch (error) { + console.error('Error calling onError callback for websocketPtr: ', socketPtr, ' Error: ', error); + } + }; + instance.socket.onclose = function (event) { + try { + // console.log('WebSocket connection closed for websocketPtr: ', socketPtr, ' with code: ', event.code, ' and reason: ', event.reason); + var length = lengthBytesUTF8(event.reason) + 1; + var buffer = Module._malloc(length); + stringToUTF8(event.reason, buffer, length); + + try { + Module.dynCall_viii(instance.onCloseCallback, socketPtr, event.code, buffer); + } finally { + Module._free(buffer); + } + } catch (error) { + console.error('Error calling onClose callback for websocketPtr: ', socketPtr, ' Error: ', error); + } + }; + // console.log('Connecting WebSocket connection for websocketPtr: ', socketPtr); + } catch (error) { + console.error('Error connecting WebSocket connection for websocketPtr: ', socketPtr, ' Error: ', error); + } + }, + /** + * Send data to the WebSocket connection. + * @param socketPtr - A pointer to the WebSocket object. IntPtr in C#. + * @param data - A pointer to the data to send. + * @param length - The length of the data to send. + */ + WebSocket_SendData: function (socketPtr, data, length) { + try { + var instance = webSockets[socketPtr]; + + if (!instance || !instance.socket || instance.socket.readyState !== 1) { + console.error('WebSocket connection does not exist for websocketPtr: ', socketPtr); + return; + } + + // console.log('Sending message to WebSocket connection for websocketPtr: ', socketPtr, ' with data: ', data, ' and length: ', length); + instance.socket.send(buffer.slice(data, data + length)); + } catch (error) { + console.error('Error sending message to WebSocket connection for websocketPtr: ', socketPtr, ' Error: ', error); + } + }, + /** + * Send a string to the WebSocket connection. + * @param socketPtr - A pointer to the WebSocket object. IntPtr in C#. + * @param data - The string to send. + */ + WebSocket_SendString: function (socketPtr, data) { + try { + var instance = webSockets[socketPtr]; + + if (!instance || !instance.socket || instance.socket.readyState !== 1) { + console.error('WebSocket connection does not exist for websocketPtr: ', socketPtr); + return; + } + + var dataStr = UTF8ToString(data); + // console.log('Sending message to WebSocket connection for websocketPtr: ', socketPtr, ' with data: ', dataStr); + instance.socket.send(dataStr); + } catch (error) { + console.error('Error sending message to WebSocket connection for websocketPtr: ', socketPtr, ' Error: ', error); + } + }, + /** + * Close the WebSocket connection. + * @param socketPtr - A pointer to the WebSocket object. IntPtr in C#. + * @param code - The status code for the close. + * @param reason - The reason for the close. + */ + WebSocket_Close: function (socketPtr, code, reason) { + try { + var instance = webSockets[socketPtr]; + + if (!instance || !instance.socket || instance.socket.readyState >= 2) { + console.error('WebSocket connection already closed for websocketPtr: ', socketPtr); + return; + } + + var reasonStr = UTF8ToString(reason); + // console.log('Closing WebSocket connection for websocketPtr: ', socketPtr, ' with code: ', code, ' and reason: ', reasonStr); + instance.socket.close(code, reasonStr); + } catch (error) { + console.error('Error closing WebSocket connection for websocketPtr: ', socketPtr, ' Error: ', error); + } + }, + /** + * Destroy a WebSocket object. + * @param socketPtr - A pointer to the WebSocket object. IntPtr in C#. + */ + WebSocket_Dispose: function (socketPtr) { + try { + // console.log('Disposing WebSocket object with websocketPtr: ', socketPtr); + delete webSockets[socketPtr]; + } catch (error) { + console.error('Error disposing WebSocket object with websocketPtr: ', socketPtr, ' Error: ', error); + } + } +}; + +autoAddDeps(UnityWebSocketLibrary, '$ptrIndex'); +autoAddDeps(UnityWebSocketLibrary, '$webSockets'); +mergeInto(LibraryManager.library, UnityWebSocketLibrary); diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins/WebSocket.jslib.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins/WebSocket.jslib.meta new file mode 100644 index 0000000..b0d60b5 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Plugins/WebSocket.jslib.meta @@ -0,0 +1,32 @@ +fileFormatVersion: 2 +guid: 0989d70b042875249a815faa52ebd8ce +PluginImporter: + externalObjects: {} + serializedVersion: 2 + iconMap: {} + executionOrder: {} + defineConstraints: [] + isPreloaded: 0 + isOverridable: 1 + isExplicitlyReferenced: 0 + validateReferences: 1 + platformData: + - first: + Any: + second: + enabled: 0 + settings: {} + - first: + Editor: Editor + second: + enabled: 0 + settings: + DefaultValueInitialized: true + - first: + WebGL: WebGL + second: + enabled: 1 + settings: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/State.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/State.cs new file mode 100644 index 0000000..ddacc76 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/State.cs @@ -0,0 +1,32 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +namespace Utilities.WebSockets +{ + /// + /// Indicates the state of the + /// + /// + /// The values of this enumeration are defined in + /// + public enum State : ushort + { + /// + /// The connection has not yet been established. + /// + Connecting = 0, + /// + /// The connection has been established and communication is possible. + /// + Open = 1, + /// + /// The connection is going through the closing handshake or close has been requested. + /// + Closing = 2, + /// + /// The connection has been closed or could not be opened. + /// + Closed = 3 + } +} diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/State.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/State.cs.meta new file mode 100644 index 0000000..38929d1 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/State.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca7645d177c3b1a4dab2d88dc2da8f56 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Runtime/ProjectScope.ProjectName.asmdef b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Utilities.Websockets.asmdef similarity index 63% rename from ProjectScope.ProjectName/Packages/com.projectscope.projectname/Runtime/ProjectScope.ProjectName.asmdef rename to Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Utilities.Websockets.asmdef index 966cd46..6757be2 100644 --- a/ProjectScope.ProjectName/Packages/com.projectscope.projectname/Runtime/ProjectScope.ProjectName.asmdef +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Utilities.Websockets.asmdef @@ -1,7 +1,9 @@ { - "name": "ProjectScope.ProjectName", - "rootNamespace": "ProjectScope.ProjectName", - "references": [], + "name": "Utilities.WebSockets", + "rootNamespace": "Utilities.WebSockets", + "references": [ + "GUID:a6609af893242c7438d701ddd4cce46a" + ], "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Utilities.Websockets.asmdef.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Utilities.Websockets.asmdef.meta new file mode 100644 index 0000000..2396c7d --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/Utilities.Websockets.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9fb4e1e06cb4c804ebfb0cff2b90e6d3 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket.cs new file mode 100644 index 0000000..c294701 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket.cs @@ -0,0 +1,286 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if !PLATFORM_WEBGL || UNITY_EDITOR + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using Utilities.Async; + +namespace Utilities.WebSockets +{ + public class WebSocket : IWebSocket + { + public WebSocket(string url, IReadOnlyList subProtocols = null) + : this(new Uri(url), subProtocols) + { + } + + public WebSocket(Uri uri, IReadOnlyList subProtocols = null) + { + var protocol = uri.Scheme; + + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + { + throw new ArgumentException($"Unsupported protocol: {protocol}"); + } + + Address = uri; + SubProtocols = subProtocols ?? new List(); + _socket = new ClientWebSocket(); + RunMessageQueue(); + } + + private async void RunMessageQueue() + { + while (_semaphore != null) + { + // syncs with update loop + await Awaiters.UnityMainThread; + + while (_events.TryDequeue(out var action)) + { + try + { + action.Invoke(); + } + catch (Exception e) + { + Debug.LogException(e); + OnError?.Invoke(e); + } + } + } + } + + ~WebSocket() + { + Dispose(false); + } + + #region IDisposable + + private void Dispose(bool disposing) + { + if (disposing) + { + lock (_lock) + { + if (State == State.Open) + { + CloseAsync().Wait(); + } + + _socket?.Dispose(); + _socket = null; + + _lifetimeCts?.Cancel(); + _lifetimeCts?.Dispose(); + _lifetimeCts = null; + + _semaphore?.Dispose(); + _semaphore = null; + } + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable + + /// + public event Action OnOpen; + + /// + public event Action OnMessage; + + /// + public event Action OnError; + + /// + public event Action OnClose; + + /// + public Uri Address { get; } + + /// + public IReadOnlyList SubProtocols { get; } + + /// + public State State => _socket?.State switch + { + WebSocketState.Connecting => State.Connecting, + WebSocketState.Open => State.Open, + WebSocketState.CloseSent or WebSocketState.CloseReceived => State.Closing, + _ => State.Closed + }; + + private object _lock = new(); + private ClientWebSocket _socket; + private SemaphoreSlim _semaphore = new(1, 1); + private CancellationTokenSource _lifetimeCts; + private readonly ConcurrentQueue _events = new(); + + /// + public async void Connect() + => await ConnectAsync(); + + /// + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + try + { + if (State == State.Open) + { + Debug.LogWarning("Websocket is already open!"); + return; + } + + _lifetimeCts?.Cancel(); + _lifetimeCts?.Dispose(); + _lifetimeCts = new CancellationTokenSource(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_lifetimeCts.Token, cancellationToken); + + foreach (var subProtocol in SubProtocols) + { + _socket.Options.AddSubProtocol(subProtocol); + } + + await _socket.ConnectAsync(Address, cts.Token).ConfigureAwait(false); + _events.Enqueue(() => OnOpen?.Invoke()); + var buffer = new Memory(new byte[8192]); + + while (State == State.Open) + { + ValueWebSocketReceiveResult result; + using var stream = new MemoryStream(); + + do + { + result = await _socket.ReceiveAsync(buffer, cts.Token).ConfigureAwait(false); + stream.Write(buffer.Span[..result.Count]); + } while (!result.EndOfMessage); + + await stream.FlushAsync(cts.Token).ConfigureAwait(false); + var memory = new ReadOnlyMemory(stream.GetBuffer(), 0, (int)stream.Length); + + if (result.MessageType != WebSocketMessageType.Close) + { + _events.Enqueue(() => OnMessage?.Invoke(new DataFrame((OpCode)(int)result.MessageType, memory))); + } + else + { + await CloseAsync(cancellationToken: CancellationToken.None).ConfigureAwait(false); + break; + } + } + + try + { + await _semaphore.WaitAsync(CancellationToken.None).ConfigureAwait(false); + } + finally + { + _semaphore.Release(); + } + } + catch (Exception e) + { + switch (e) + { + case TaskCanceledException: + case OperationCanceledException: + break; + default: + Debug.LogException(e); + _events.Enqueue(() => OnError?.Invoke(e)); + _events.Enqueue(() => OnClose?.Invoke(CloseStatusCode.AbnormalClosure, e.Message)); + break; + } + } + } + + /// + public async Task SendAsync(string text, CancellationToken cancellationToken = default) + => await Internal_SendAsync(Encoding.UTF8.GetBytes(text), WebSocketMessageType.Text, cancellationToken); + + /// + public async Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default) + => await Internal_SendAsync(data, WebSocketMessageType.Binary, cancellationToken); + + private async Task Internal_SendAsync(ArraySegment data, WebSocketMessageType opCode, CancellationToken cancellationToken) + { + try + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(_lifetimeCts.Token, cancellationToken); + await _semaphore.WaitAsync(cts.Token).ConfigureAwait(false); + + if (State != State.Open) + { + throw new InvalidOperationException("WebSocket is not ready!"); + } + + await _socket.SendAsync(data, opCode, true, cts.Token).ConfigureAwait(false); + } + catch (Exception e) + { + switch (e) + { + case TaskCanceledException: + case OperationCanceledException: + break; + default: + Debug.LogException(e); + _events.Enqueue(() => OnError?.Invoke(e)); + break; + } + } + finally + { + _semaphore.Release(); + } + } + + /// + public async void Close() + => await CloseAsync(); + + /// + public async Task CloseAsync(CloseStatusCode code = CloseStatusCode.Normal, string reason = "", CancellationToken cancellationToken = default) + { + try + { + if (State == State.Open) + { + await _socket.CloseAsync((WebSocketCloseStatus)(int)code, reason, cancellationToken).ConfigureAwait(false); + _events.Enqueue(() => OnClose?.Invoke(code, reason)); + } + } + catch (Exception e) + { + switch (e) + { + case TaskCanceledException: + case OperationCanceledException: + break; + default: + Debug.LogException(e); + _events.Enqueue(() => OnError?.Invoke(e)); + break; + } + } + } + } +} +#endif // !PLATFORM_WEBGL || UNITY_EDITOR diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket.cs.meta new file mode 100644 index 0000000..bbc7a89 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 21cd68194448d4542bb0ce2ed9b1acd5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket_WebGL.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket_WebGL.cs new file mode 100644 index 0000000..18e3717 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket_WebGL.cs @@ -0,0 +1,264 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#if PLATFORM_WEBGL && !UNITY_EDITOR + +using AOT; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; +using Utilities.Async; + +namespace Utilities.WebSockets +{ + public class WebSocket : IWebSocket + { + public WebSocket(string url, IReadOnlyList subProtocols = null) + : this(new Uri(url), subProtocols) + { + } + + public WebSocket(Uri uri, IReadOnlyList subProtocols = null) + { + var protocol = uri.Scheme; + + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + { + throw new ArgumentException($"Unsupported protocol: {protocol}"); + } + + Address = uri; + SubProtocols = subProtocols ?? new List(); + _socket = WebSocket_Create(uri.ToString(), string.Join(',', SubProtocols), WebSocket_OnOpen, WebSocket_OnMessage, WebSocket_OnError, WebSocket_OnClose); + + if (_socket == IntPtr.Zero || !_sockets.TryAdd(_socket, this)) + { + throw new InvalidOperationException("Failed to create WebSocket instance!"); + } + + RunMessageQueue(); + } + + ~WebSocket() + { + Dispose(false); + } + + private async void RunMessageQueue() + { + while (_semaphore != null) + { + // syncs with update loop + await Awaiters.UnityMainThread; + + while (_events.TryDequeue(out var action)) + { + try + { + action.Invoke(); + } + catch (Exception e) + { + Debug.LogException(e); + OnError?.Invoke(e); + } + } + } + } + + #region IDisposable + + private void Dispose(bool disposing) + { + if (disposing) + { + lock (_lock) + { + if (State == State.Open) + { + CloseAsync().Wait(); + } + + WebSocket_Dispose(_socket); + _socket = IntPtr.Zero; + + _lifetimeCts?.Cancel(); + _lifetimeCts?.Dispose(); + _lifetimeCts = null; + + _semaphore?.Dispose(); + _semaphore = null; + } + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion IDisposable + + #region Native Interop + + private static ConcurrentDictionary _sockets = new(); + + [DllImport("__Internal")] + private static extern IntPtr WebSocket_Create(string url, string subProtocols, WebSocket_OnOpenDelegate onOpen, WebSocket_OnMessageDelegate onMessage, WebSocket_OnErrorDelegate onError, WebSocket_OnCloseDelegate onClose); + + private delegate void WebSocket_OnOpenDelegate(IntPtr websocketPtr); + + [MonoPInvokeCallback(typeof(WebSocket_OnOpenDelegate))] + private static void WebSocket_OnOpen(IntPtr websocketPtr) + { + if (_sockets.TryGetValue(websocketPtr, out var socket)) + { + socket._events.Enqueue(() => socket.OnOpen?.Invoke()); + } + else + { + Debug.LogError($"{nameof(WebSocket_OnOpen)}: Invalid websocket pointer! {websocketPtr.ToInt64()}"); + } + } + + private delegate void WebSocket_OnMessageDelegate(IntPtr websocketPtr, IntPtr dataPtr, int length, OpCode type); + + [MonoPInvokeCallback(typeof(WebSocket_OnMessageDelegate))] + private static void WebSocket_OnMessage(IntPtr websocketPtr, IntPtr dataPtr, int length, OpCode type) + { + if (_sockets.TryGetValue(websocketPtr, out var socket)) + { + var buffer = new byte[length]; + Marshal.Copy(dataPtr, buffer, 0, length); + socket._events.Enqueue(() => socket.OnMessage?.Invoke(new DataFrame(type, buffer))); + } + else + { + Debug.LogError($"{nameof(WebSocket_OnMessage)}: Invalid websocket pointer! {websocketPtr.ToInt64()}"); + } + } + + private delegate void WebSocket_OnErrorDelegate(IntPtr websocketPtr, IntPtr messagePtr); + + [MonoPInvokeCallback(typeof(WebSocket_OnErrorDelegate))] + private static void WebSocket_OnError(IntPtr websocketPtr, IntPtr messagePtr) + { + if (_sockets.TryGetValue(websocketPtr, out var socket)) + { + var message = Marshal.PtrToStringUTF8(messagePtr); + socket._events.Enqueue(() => socket.OnError?.Invoke(new Exception(message))); + } + else + { + Debug.LogError($"{nameof(WebSocket_OnError)}: Invalid websocket pointer! {websocketPtr.ToInt64()}"); + } + } + + private delegate void WebSocket_OnCloseDelegate(IntPtr websocketPtr, CloseStatusCode code, IntPtr reasonPtr); + + [MonoPInvokeCallback(typeof(WebSocket_OnCloseDelegate))] + private static void WebSocket_OnClose(IntPtr websocketPtr, CloseStatusCode code, IntPtr reasonPtr) + { + if (_sockets.TryGetValue(websocketPtr, out var socket)) + { + var reason = Marshal.PtrToStringUTF8(reasonPtr); + socket._events.Enqueue(() => socket.OnClose?.Invoke(code, reason)); + } + else + { + Debug.LogError($"{nameof(WebSocket_OnClose)}: Invalid websocket pointer! {websocketPtr.ToInt64()}"); + } + } + + [DllImport("__Internal")] + private static extern int WebSocket_GetState(IntPtr websocketPtr); + + [DllImport("__Internal")] + private static extern void WebSocket_Connect(IntPtr websocketPtr); + + [DllImport("__Internal")] + private static extern void WebSocket_SendData(IntPtr websocketPtr, byte[] data, int length); + + [DllImport("__Internal")] + private static extern void WebSocket_SendString(IntPtr websocketPtr, string text); + + [DllImport("__Internal")] + private static extern void WebSocket_Close(IntPtr websocketPtr, CloseStatusCode code, string reason); + + [DllImport("__Internal")] + private static extern void WebSocket_Dispose(IntPtr websocketPtr); + + #endregion Native Interop + + /// + public event Action OnOpen; + + /// + public event Action OnMessage; + + /// + public event Action OnError; + + /// + public event Action OnClose; + + /// + public Uri Address { get; } + + /// + public IReadOnlyList SubProtocols { get; } + + /// + public State State => _socket != IntPtr.Zero + ? (State)WebSocket_GetState(_socket) + : State.Closed; + + private object _lock = new(); + private IntPtr _socket; + private SemaphoreSlim _semaphore = new(1, 1); + private CancellationTokenSource _lifetimeCts; + private readonly ConcurrentQueue _events = new(); + + /// + public async void Connect() + => await ConnectAsync(); + + /// + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + WebSocket_Connect(_socket); + await Task.CompletedTask; + } + + /// + public async Task SendAsync(string text, CancellationToken cancellationToken = default) + { + WebSocket_SendString(_socket, text); + await Task.CompletedTask; + } + + /// + public async Task SendAsync(ArraySegment data, CancellationToken cancellationToken = default) + { + WebSocket_SendData(_socket, data.Array, data.Count); + await Task.CompletedTask; + } + + /// + public async void Close() + => await CloseAsync(); + + /// + public async Task CloseAsync(CloseStatusCode code = CloseStatusCode.Normal, string reason = "", CancellationToken cancellationToken = default) + { + WebSocket_Close(_socket, code, reason); + await Task.CompletedTask; + } + } +} +#endif // PLATFORM_WEBGL && !UNITY_EDITOR diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket_WebGL.cs.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket_WebGL.cs.meta new file mode 100644 index 0000000..c79aae4 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Runtime/WebSocket_WebGL.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f165aea53e8eb7547bb3053a78570d70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo.meta new file mode 100644 index 0000000..0f2cd7b --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 687c34e3b9e88cc459456398119546e0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/DefaultRuntimeTheme.tss b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/DefaultRuntimeTheme.tss new file mode 100644 index 0000000..79453c7 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/DefaultRuntimeTheme.tss @@ -0,0 +1,2 @@ +@import url("unity-theme://default"); +VisualElement {} \ No newline at end of file diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/DefaultRuntimeTheme.tss.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/DefaultRuntimeTheme.tss.meta new file mode 100644 index 0000000..20c8290 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/DefaultRuntimeTheme.tss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: de08e26de5e540e4a90b6be51bf83b27 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/Utilities.WebSockets.Sample.asmdef b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/Utilities.WebSockets.Sample.asmdef new file mode 100644 index 0000000..836e370 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/Utilities.WebSockets.Sample.asmdef @@ -0,0 +1,16 @@ +{ + "name": "Utilities.WebSockets.Sample", + "rootNamespace": "Utilities.WebSockets.Sample", + "references": [ + "GUID:9fb4e1e06cb4c804ebfb0cff2b90e6d3" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/Utilities.WebSockets.Sample.asmdef.meta b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/Utilities.WebSockets.Sample.asmdef.meta new file mode 100644 index 0000000..09c4327 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/Utilities.WebSockets.Sample.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3354db43e4e35024e83586c087159d2b +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/WebSocketBindings.cs b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/WebSocketBindings.cs new file mode 100644 index 0000000..a78d236 --- /dev/null +++ b/Utilities.Websockets/Packages/com.utilities.websockets/Samples~/WebsocketDemo/WebSocketBindings.cs @@ -0,0 +1,331 @@ +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Utilities.WebSockets.Sample +{ + [RequireComponent(typeof(UIDocument))] + public class WebSocketBindings : MonoBehaviour + { + [SerializeField] + private UIDocument uiDocument; + + [SerializeField] + private string address = "wss://echo.websocket.events"; + + private Label statusLabel; + private Label fpsLabel; + private TextField addressTextField; + private Button connectButton; + private Button disconnectButton; + private TextField sendMessageTextField; + private VisualElement sendMessageButtonGroup; + private Button sendTextButton; + private Button sendBytesButton; + private Button sendText1000Button; + private Button sendBytes1000Button; + private Toggle logMessagesToggle; + private Label sendCountLabel; + private Label receiveCountLabel; + private Button clearLogsButton; + private ListView messageListView; + + private int frame; + private int sendCount; + private int receiveCount; + + private float time; + private float fps; + + private WebSocket webSocket; + + private readonly List> logs = new(); + + private void OnValidate() + { + if (!uiDocument) + { + uiDocument = GetComponent(); + } + } + + private void Awake() + { + OnValidate(); + + var root = uiDocument.rootVisualElement; + + statusLabel = root.Q