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