diff --git a/unity/tanks-fishnet/.dockerignore b/unity/tanks-fishnet/.dockerignore
index 75becf1..1515742 100644
--- a/unity/tanks-fishnet/.dockerignore
+++ b/unity/tanks-fishnet/.dockerignore
@@ -1,2 +1,2 @@
**
-!build/LinuxServer/
+!builds/LinuxServer/
diff --git a/unity/tanks-fishnet/.vscode/extensions.json b/unity/tanks-fishnet/.vscode/extensions.json
new file mode 100644
index 0000000..ddb6ff8
--- /dev/null
+++ b/unity/tanks-fishnet/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "visualstudiotoolsforunity.vstuc"
+ ]
+}
diff --git a/unity/tanks-fishnet/.vscode/launch.json b/unity/tanks-fishnet/.vscode/launch.json
new file mode 100644
index 0000000..da60e25
--- /dev/null
+++ b/unity/tanks-fishnet/.vscode/launch.json
@@ -0,0 +1,10 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Attach to Unity",
+ "type": "vstuc",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/.vscode/settings.json b/unity/tanks-fishnet/.vscode/settings.json
index 6a0a9db..e6fa078 100644
--- a/unity/tanks-fishnet/.vscode/settings.json
+++ b/unity/tanks-fishnet/.vscode/settings.json
@@ -1,57 +1,56 @@
{
- "files.exclude":
- {
- "**/.DS_Store":true,
- "**/.git":true,
- "**/.gitmodules":true,
- "**/*.booproj":true,
- "**/*.pidb":true,
- "**/*.suo":true,
- "**/*.user":true,
- "**/*.userprefs":true,
- "**/*.unityproj":true,
- "**/*.dll":true,
- "**/*.exe":true,
- "**/*.pdf":true,
- "**/*.mid":true,
- "**/*.midi":true,
- "**/*.wav":true,
- "**/*.gif":true,
- "**/*.ico":true,
- "**/*.jpg":true,
- "**/*.jpeg":true,
- "**/*.png":true,
- "**/*.psd":true,
- "**/*.tga":true,
- "**/*.tif":true,
- "**/*.tiff":true,
- "**/*.3ds":true,
- "**/*.3DS":true,
- "**/*.fbx":true,
- "**/*.FBX":true,
- "**/*.lxo":true,
- "**/*.LXO":true,
- "**/*.ma":true,
- "**/*.MA":true,
- "**/*.obj":true,
- "**/*.OBJ":true,
- "**/*.asset":true,
- "**/*.cubemap":true,
- "**/*.flare":true,
- "**/*.mat":true,
- "**/*.meta":true,
- "**/*.prefab":true,
- "**/*.unity":true,
- "build/":true,
- "Build/":true,
- "Library/":true,
- "library/":true,
- "obj/":true,
- "Obj/":true,
- "ProjectSettings/":true,
- "temp/":true,
- "Temp/":true
+ "files.exclude": {
+ "**/.DS_Store": true,
+ "**/.git": true,
+ "**/.gitmodules": true,
+ "**/*.booproj": true,
+ "**/*.pidb": true,
+ "**/*.suo": true,
+ "**/*.user": true,
+ "**/*.userprefs": true,
+ "**/*.unityproj": true,
+ "**/*.dll": true,
+ "**/*.exe": true,
+ "**/*.pdf": true,
+ "**/*.mid": true,
+ "**/*.midi": true,
+ "**/*.wav": true,
+ "**/*.gif": true,
+ "**/*.ico": true,
+ "**/*.jpg": true,
+ "**/*.jpeg": true,
+ "**/*.png": true,
+ "**/*.psd": true,
+ "**/*.tga": true,
+ "**/*.tif": true,
+ "**/*.tiff": true,
+ "**/*.3ds": true,
+ "**/*.3DS": true,
+ "**/*.fbx": true,
+ "**/*.FBX": true,
+ "**/*.lxo": true,
+ "**/*.LXO": true,
+ "**/*.ma": true,
+ "**/*.MA": true,
+ "**/*.obj": true,
+ "**/*.OBJ": true,
+ "**/*.asset": true,
+ "**/*.cubemap": true,
+ "**/*.flare": true,
+ "**/*.mat": true,
+ "**/*.meta": true,
+ "**/*.prefab": true,
+ "**/*.unity": true,
+ "build/": true,
+ "Build/": true,
+ "Library/": true,
+ "library/": true,
+ "obj/": true,
+ "Obj/": true,
+ "ProjectSettings/": true,
+ "temp/": true,
+ "Temp/": true
},
- "dotnet.defaultSolution": "Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln",
+ "dotnet.defaultSolution": "tanks-fishnet.sln",
"cmake.sourceDirectory": "C:/Users/micro/Unity Projects/example-tanks-unity-mirror/Library/PackageCache/com.unity.ide.visualstudio@2.0.18/Editor/COMIntegration/COMIntegration~"
}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/FishNet/Plugins/CodeAnalysis/FishNet.CodeAnalysis.Analyzers.dll.meta b/unity/tanks-fishnet/Assets/FishNet/Plugins/CodeAnalysis/FishNet.CodeAnalysis.Analyzers.dll.meta
deleted file mode 100644
index 73913eb..0000000
--- a/unity/tanks-fishnet/Assets/FishNet/Plugins/CodeAnalysis/FishNet.CodeAnalysis.Analyzers.dll.meta
+++ /dev/null
@@ -1,72 +0,0 @@
-fileFormatVersion: 2
-guid: 1907658b89c1bbe42a0063df40b7ca24
-labels:
-- RoslynAnalyzer
-PluginImporter:
- externalObjects: {}
- serializedVersion: 2
- iconMap: {}
- executionOrder: {}
- defineConstraints:
- - UNITY_2020_3_OR_NEWER
- isPreloaded: 0
- isOverridable: 0
- isExplicitlyReferenced: 0
- validateReferences: 1
- platformData:
- - first:
- : Any
- second:
- enabled: 0
- settings:
- Exclude Editor: 1
- Exclude Linux64: 1
- Exclude OSXUniversal: 1
- Exclude Win: 1
- Exclude Win64: 1
- - first:
- Any:
- second:
- enabled: 0
- settings: {}
- - first:
- Editor: Editor
- second:
- enabled: 0
- settings:
- CPU: AnyCPU
- DefaultValueInitialized: true
- OS: AnyOS
- - first:
- Standalone: Linux64
- second:
- enabled: 0
- settings:
- CPU: None
- - first:
- Standalone: OSXUniversal
- second:
- enabled: 0
- settings:
- CPU: None
- - first:
- Standalone: Win
- second:
- enabled: 0
- settings:
- CPU: None
- - first:
- Standalone: Win64
- second:
- enabled: 0
- settings:
- CPU: None
- - first:
- Windows Store Apps: WindowsStoreApps
- second:
- enabled: 0
- settings:
- CPU: AnyCPU
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/FishNet/Plugins/CodeAnalysis/FishNet.CodeAnalysis.dll.meta b/unity/tanks-fishnet/Assets/FishNet/Plugins/CodeAnalysis/FishNet.CodeAnalysis.dll.meta
deleted file mode 100644
index 9d99248..0000000
--- a/unity/tanks-fishnet/Assets/FishNet/Plugins/CodeAnalysis/FishNet.CodeAnalysis.dll.meta
+++ /dev/null
@@ -1,33 +0,0 @@
-fileFormatVersion: 2
-guid: 620557a5e202e644cb322b8fcc9422ea
-PluginImporter:
- externalObjects: {}
- serializedVersion: 2
- iconMap: {}
- executionOrder: {}
- defineConstraints: []
- isPreloaded: 0
- isOverridable: 0
- isExplicitlyReferenced: 0
- validateReferences: 1
- platformData:
- - first:
- Any:
- second:
- enabled: 1
- settings: {}
- - first:
- Editor: Editor
- second:
- enabled: 0
- settings:
- DefaultValueInitialized: true
- - first:
- Windows Store Apps: WindowsStoreApps
- second:
- enabled: 0
- settings:
- CPU: AnyCPU
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/NetPeer.cs b/unity/tanks-fishnet/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/NetPeer.cs
index 0ebf508..1085b81 100644
--- a/unity/tanks-fishnet/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/NetPeer.cs
+++ b/unity/tanks-fishnet/Assets/FishNet/Runtime/Transporting/Transports/Tugboat/LiteNetLib/NetPeer.cs
@@ -17,11 +17,11 @@ namespace LiteNetLib
[Flags]
public enum ConnectionState : byte
{
- Outgoing = 1 << 1,
- Connected = 1 << 2,
+ Outgoing = 1 << 1,
+ Connected = 1 << 2,
ShutdownRequested = 1 << 3,
- Disconnected = 1 << 4,
- EndPointChange = 1 << 5,
+ Disconnected = 1 << 4,
+ EndPointChange = 1 << 5,
Any = Outgoing | Connected | ShutdownRequested | EndPointChange
}
@@ -161,7 +161,7 @@ private class IncomingFragments
///
/// Current one-way ping (RTT/2) in milliseconds
///
- public int Ping => _avgRtt/2;
+ public int Ping => _avgRtt / 2;
///
/// Round trip time in milliseconds
@@ -212,7 +212,7 @@ internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id)
_connectionState = ConnectionState.Connected;
_mergeData = new NetPacket(PacketProperty.Merged, NetConstants.MaxPacketSize);
_pongPacket = new NetPacket(PacketProperty.Pong, 0);
- _pingPacket = new NetPacket(PacketProperty.Ping, 0) {Sequence = 1};
+ _pingPacket = new NetPacket(PacketProperty.Ping, 0) { Sequence = 1 };
_unreliableChannel = new Queue();
_holdedFragments = new Dictionary();
@@ -268,7 +268,7 @@ private void OverrideMtu(int mtuValue)
public int GetPacketsCountInReliableQueue(byte channelNumber, bool ordered)
{
int idx = channelNumber * NetConstants.ChannelTypeCount +
- (byte) (ordered ? DeliveryMethod.ReliableOrdered : DeliveryMethod.ReliableUnordered);
+ (byte)(ordered ? DeliveryMethod.ReliableOrdered : DeliveryMethod.ReliableUnordered);
var channel = _channels[idx];
return channel != null ? ((ReliableChannel)channel).PacketsInQueue : 0;
}
@@ -312,7 +312,7 @@ public void SendPooledPacket(PooledPacket packet, int userDataSize)
}
else
{
- lock(_unreliableChannel)
+ lock (_unreliableChannel)
_unreliableChannel.Enqueue(packet._packet);
}
}
@@ -626,7 +626,7 @@ private void Send_Internal(
ushort currentFragmentId = (ushort)Interlocked.Increment(ref _fragmentId);
- for(ushort partIdx = 0; partIdx < totalPackets; partIdx++)
+ for (ushort partIdx = 0; partIdx < totalPackets; partIdx++)
{
int sendLength = length > packetDataSize ? packetDataSize : length;
@@ -654,7 +654,7 @@ private void Send_Internal(
if (channel == null) //unreliable
{
- lock(_unreliableChannel)
+ lock (_unreliableChannel)
_unreliableChannel.Enqueue(packet);
}
else
@@ -861,7 +861,7 @@ internal ShutdownResult Shutdown(byte[] data, int start, int length, bool force)
Interlocked.Exchange(ref _timeSinceLastPacket, 0);
//send shutdown packet
- _shutdownPacket = new NetPacket(PacketProperty.Disconnect, length) {ConnectionNumber = _connectNum};
+ _shutdownPacket = new NetPacket(PacketProperty.Disconnect, length) { ConnectionNumber = _connectNum };
FastBitConverter.GetBytes(_shutdownPacket.RawData, 1, _connectTime);
if (_shutdownPacket.Size >= _mtu)
{
@@ -883,7 +883,7 @@ private void UpdateRoundTripTime(int roundTripTime)
{
_rtt += roundTripTime;
_rttCount++;
- _avgRtt = _rtt/_rttCount;
+ _avgRtt = _rtt / _rttCount;
_resendDelay = 25.0 + _avgRtt * 2.1; // 25 ms + double rtt
}
@@ -939,7 +939,7 @@ internal void AddReliablePacket(DeliveryMethod method, NetPacket p)
var fragment = fragments[i];
int writtenSize = fragment.Size - NetConstants.FragmentedHeaderTotalSize;
- if (pos+writtenSize > resultingPacket.RawData.Length)
+ if (pos + writtenSize > resultingPacket.RawData.Length)
{
_holdedFragments.Remove(packetFragId);
NetDebug.WriteError("Fragment error pos: {0} >= resultPacketSize: {1} , totalSize: {2}",
@@ -1003,7 +1003,7 @@ private void ProcessMtuPacket(NetPacket packet)
packet.Property = PacketProperty.MtuOk;
NetManager.SendRawAndRecycle(packet, _remoteEndPoint);
}
- else if(receivedMtu > _mtu && !_finishMtu) //MtuOk
+ else if (receivedMtu > _mtu && !_finishMtu) //MtuOk
{
//invalid packet
if (receivedMtu != NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer)
@@ -1011,7 +1011,7 @@ private void ProcessMtuPacket(NetPacket packet)
lock (_mtuMutex)
{
- SetMtu(_mtuIdx+1);
+ SetMtu(_mtuIdx + 1);
}
//if maxed - finish.
if (_mtuIdx == NetConstants.PossibleMtu.Length - 1)
@@ -1073,7 +1073,7 @@ internal ConnectRequestResult ProcessConnectRequest(NetConnectRequestPacket conn
{
var remoteBytes = _remoteEndPoint.Serialize();
var localBytes = connRequest.TargetAddress;
- for (int i = remoteBytes.Size-1; i >= 0; i--)
+ for (int i = remoteBytes.Size - 1; i >= 0; i--)
{
byte rb = remoteBytes[i];
if (rb == localBytes[i])
@@ -1173,7 +1173,7 @@ internal void ProcessPacket(NetPacket packet)
{
_pingTimer.Stop();
int elapsedMs = (int)_pingTimer.ElapsedMilliseconds;
- _remoteDelta = BitConverter.ToInt64(packet.RawData, 3) + (elapsedMs * TimeSpan.TicksPerMillisecond ) / 2 - DateTime.UtcNow.Ticks;
+ _remoteDelta = BitConverter.ToInt64(packet.RawData, 3) + (elapsedMs * TimeSpan.TicksPerMillisecond) / 2 - DateTime.UtcNow.Ticks;
UpdateRoundTripTime(elapsedMs);
NetManager.ConnectionLatencyUpdated(this, elapsedMs / 2);
NetDebug.Write("[PP]Ping: {0} - {1} - {2}", packet.Sequence, elapsedMs, _remoteDelta);
diff --git a/unity/tanks-fishnet/Assets/LobbyConfig.cs b/unity/tanks-fishnet/Assets/LobbyConfig.cs
index f01ffc4..cb52342 100644
--- a/unity/tanks-fishnet/Assets/LobbyConfig.cs
+++ b/unity/tanks-fishnet/Assets/LobbyConfig.cs
@@ -1,5 +1,6 @@
using FishNet.Object;
using FishNet.Object.Synchronizing;
+using Rivet;
using UnityEngine;
public enum GameMode
@@ -12,6 +13,7 @@ public enum GameMode
public class LobbyConfig : NetworkBehaviour
{
private RivetManager _rm;
+ private Server _server;
[SyncVar] public GameMode gameMode = GameMode.ModeA;
[SyncVar] public float moveSpeed = 5f;
@@ -19,6 +21,7 @@ public class LobbyConfig : NetworkBehaviour
void Start()
{
_rm = FindObjectOfType();
+ _server = FindObjectOfType();
if (IsServer)
{
@@ -31,7 +34,7 @@ void Start()
public override void OnStartNetwork()
{
base.OnStartNetwork();
-
+
// Update UI
if (IsClient)
{
@@ -50,7 +53,7 @@ private void UpdateConfig()
// Update game mode
if (!Application.isEditor)
{
- switch (_rm.gameModeName)
+ switch (_server.gameModeName)
{
case "mode-a":
gameMode = GameMode.ModeA;
@@ -62,13 +65,13 @@ private void UpdateConfig()
gameMode = GameMode.Custom;
break;
default:
- Debug.LogError("Invalid game mode name: " + _rm.gameModeName);
+ Debug.LogError("Invalid game mode name: " + _server.gameModeName);
break;
}
}
// Update properties
- var lc = _rm.LobbyConfig;
+ var lc = _server.LobbyConfig;
if (lc == null) return;
moveSpeed = (float)lc["move_speed"];
}
diff --git a/unity/tanks-fishnet/Assets/Rivet.meta b/unity/tanks-fishnet/Assets/Rivet.meta
new file mode 100644
index 0000000..6f55048
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 340c7fefc29a24a948e0af4516b94e7f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Dockerfile b/unity/tanks-fishnet/Assets/Rivet/Dockerfile
new file mode 100644
index 0000000..d7e395d
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Dockerfile
@@ -0,0 +1,22 @@
+# MARK: Runner
+FROM debian:12
+WORKDIR /app
+
+# Install necessary libraries for Unity
+RUN apt-get update && apt-get install -y \
+ ca-certificates \
+ && update-ca-certificates \
+ && rm -rf /var/lib/apt/lists/* \
+ && useradd -ms /bin/bash rivet
+
+# Copy the precompiled Unity server files
+COPY builds/LinuxServer/ /app
+RUN ls /app && chmod +x /app/LinuxServer.x86_64
+
+# Change to user rivet
+USER rivet
+
+ENV UNITY_SERVER=1
+
+# Run the server
+ENTRYPOINT ["/app/LinuxServer.x86_64", "-batchmode", "-nographics"]
diff --git a/unity/tanks-fishnet/Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta b/unity/tanks-fishnet/Assets/Rivet/Dockerfile.meta
similarity index 74%
rename from unity/tanks-fishnet/Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta
rename to unity/tanks-fishnet/Assets/Rivet/Dockerfile.meta
index aa8a077..1a0b7a3 100644
--- a/unity/tanks-fishnet/Assets/FishNet/CodeGenerating/cecil-0.11.4/Mono.Cecil.sln.meta
+++ b/unity/tanks-fishnet/Assets/Rivet/Dockerfile.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 246f31a0e00fea74a93125fec6d80da8
+guid: 13b71fb9c7cd14e2294b40b89dcef233
DefaultImporter:
externalObjects: {}
userData:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor.meta b/unity/tanks-fishnet/Assets/Rivet/Editor.meta
new file mode 100644
index 0000000..4e5583c
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5043f2f84714f4e3ca72b93f6a18c333
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/BuildPipeline.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/BuildPipeline.cs
new file mode 100644
index 0000000..0d01747
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/BuildPipeline.cs
@@ -0,0 +1,54 @@
+using UnityEditor.Build;
+using UnityEditor.Build.Reporting;
+using UnityEngine;
+using System.IO;
+
+namespace Rivet
+{
+ public class BuildScript : IPreprocessBuildWithReport, IPostprocessBuildWithReport
+ {
+ public int callbackOrder { get { return 0; } }
+
+ public void OnPreprocessBuild(BuildReport report)
+ {
+ // Check if StreamingAssets folder exists and create it if it doesn't
+ string streamingAssetsPath = Application.streamingAssetsPath;
+ if (!Directory.Exists(streamingAssetsPath))
+ {
+ Directory.CreateDirectory(streamingAssetsPath);
+ }
+
+ // If either is null, add a build error
+ if (string.IsNullOrEmpty(ExtensionData.ApiEndpoint))
+ {
+ Debug.LogError("Rivet API endpoint is not set. Please set the API endpoint in the Rivet settings.");
+ }
+
+ if (string.IsNullOrEmpty(ExtensionData.RivetToken))
+ {
+ Debug.LogError("Rivet token is not set. Please set the Rivet token in the Rivet settings.");
+ }
+
+ // Create the asset file before the build
+ RivetSettings data = new RivetSettings
+ {
+ ApiEndpoint = ExtensionData.ApiEndpoint,
+ RivetToken = ExtensionData.RivetToken
+ };
+
+ string json = JsonUtility.ToJson(data);
+ string filePath = Path.Combine(Application.streamingAssetsPath, "rivet_export.json");
+ File.WriteAllText(filePath, json);
+ }
+
+ public void OnPostprocessBuild(BuildReport report)
+ {
+ // Delete the asset file after the build
+ string filePath = Path.Combine(Application.streamingAssetsPath, "rivet_export.json");
+ if (File.Exists(filePath))
+ {
+ File.Delete(filePath);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/BuildPipeline.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/BuildPipeline.cs.meta
new file mode 100644
index 0000000..e0c7a96
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/BuildPipeline.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 31fc2096397c426c68832f49183d6abf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/ExportPlugin.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/ExportPlugin.cs
new file mode 100644
index 0000000..6fde49c
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/ExportPlugin.cs
@@ -0,0 +1,15 @@
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+public class ExportPackage
+{
+ [MenuItem("Assets/Export My Plugin")]
+ public static void Export()
+ {
+ string[] projectContent = AssetDatabase.GetAllAssetPaths();
+ var assetsToExport = projectContent.Where(path => path.StartsWith("Assets/Rivet")).ToArray();
+ AssetDatabase.ExportPackage(assetsToExport, "Rivet.unitypackage", ExportPackageOptions.Recurse);
+ Debug.Log("Project Exported");
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/ExportPlugin.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/ExportPlugin.cs.meta
new file mode 100644
index 0000000..8605280
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/ExportPlugin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 54ef17bce8e7748758018b7081a61f0f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Images.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Images.meta
new file mode 100644
index 0000000..c3303eb
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Images.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 05e473322652c4d059fa03b7d47a12fb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-black.png b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-black.png
new file mode 100644
index 0000000..c91fbd1
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-black.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8938891db55c77a3cf7a1b10858076271f047639aaee4ae429ee5e953ad6ff5f
+size 2831
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-black.png.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-black.png.meta
new file mode 100644
index 0000000..76b3a14
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-black.png.meta
@@ -0,0 +1,140 @@
+fileFormatVersion: 2
+guid: 292132c72bd824b278283c5351e3cad0
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 12
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 3
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: WebGL
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Server
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-white.png b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-white.png
new file mode 100644
index 0000000..d3947b4
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-white.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b5aac287ec895eeeacb00b390033b0b3a9817c1000f1367ba70361a5c980527d
+size 2828
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-white.png.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-white.png.meta
new file mode 100644
index 0000000..a96d648
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Images/icon-text-white.png.meta
@@ -0,0 +1,140 @@
+fileFormatVersion: 2
+guid: c8a6d5bc1169a4c7ead9581401233b7b
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 12
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 3
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: WebGL
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 3
+ buildTarget: Server
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Result.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/Result.cs
new file mode 100644
index 0000000..a2515af
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Result.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+
+namespace Rivet
+{
+ // https://josef.codes/my-take-on-the-result-class-in-c-sharp/
+ public abstract class Result
+ {
+ public bool Success { get; protected set; }
+ public bool Failure => !Success;
+ }
+
+ public abstract class Result : Result
+ {
+ private T _data;
+
+ protected Result(T data)
+ {
+ Data = data;
+ }
+
+ public T Data
+ {
+ get => Success ? _data : throw new Exception($"You can't access .{nameof(Data)} when .{nameof(Success)} is false");
+ set => _data = value;
+ }
+ }
+
+ public class SuccessResult : Result
+ {
+ public SuccessResult()
+ {
+ Success = true;
+ }
+ }
+
+ public class SuccessResult : Result
+ {
+ public SuccessResult(T data) : base(data)
+ {
+ Success = true;
+ }
+ }
+
+ public class ErrorResult : Result, IErrorResult
+ {
+ public ErrorResult(string message) : this(message, Array.Empty())
+ {
+ }
+
+ public ErrorResult(string message, IReadOnlyCollection errors)
+ {
+ Message = message;
+ Success = false;
+ Errors = errors ?? Array.Empty();
+ }
+
+ public string Message { get; }
+ public IReadOnlyCollection Errors { get; }
+ }
+
+ public class ErrorResult : Result, IErrorResult
+ {
+ public ErrorResult(string message) : this(message, Array.Empty())
+ {
+ }
+
+ public ErrorResult(string message, IReadOnlyCollection errors) : base(default)
+ {
+ Message = message;
+ Success = false;
+ Errors = errors ?? Array.Empty();
+ }
+
+ public string Message { get; set; }
+ public IReadOnlyCollection Errors { get; }
+ }
+
+ public class Error
+ {
+ public Error(string details) : this(null, details)
+ {
+
+ }
+
+ public Error(string code, string details)
+ {
+ Code = code;
+ Details = details;
+ }
+
+ public string Code { get; }
+ public string Details { get; }
+ }
+
+ internal interface IErrorResult
+ {
+ string Message { get; }
+ IReadOnlyCollection Errors { get; }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Result.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Result.cs.meta
new file mode 100644
index 0000000..095a9cb
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Result.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f2e6722c072dc45198427642016e356d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/RivetCLI.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetCLI.cs
new file mode 100644
index 0000000..ed96c29
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetCLI.cs
@@ -0,0 +1,120 @@
+using System.IO;
+using Newtonsoft.Json.Linq;
+
+namespace Rivet
+{
+ public static class RivetCLI
+ {
+ public const string REQUIRED_RIVET_CLI_VERSION = "v1.0.1";
+ public const string RIVET_CLI_PATH_SETTING = "RivetCLIPath";
+
+ public static bool CLIInstalled()
+ {
+ string editorRivetPath = GetRivetCLIPath();
+
+ if (string.IsNullOrEmpty(editorRivetPath))
+ {
+ return false;
+ }
+
+ var result = RunCommand("sidekick", "get-cli-version");
+ switch (result)
+ {
+ case SuccessResult successResult:
+ // Verify that the version that came back is correct
+ if (successResult.Data["Ok"] == null)
+ {
+ return false;
+ }
+ var cliVersion = successResult.Data["Ok"]["version"].ToString();
+ if (cliVersion != REQUIRED_RIVET_CLI_VERSION)
+ {
+ return false;
+ }
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static Result RunCommand(params string[] args)
+ {
+ return RunRivetCLI(args);
+ }
+
+ public static string GetBinDir()
+ {
+ string homePath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
+ return Path.Combine(homePath, ".rivet", REQUIRED_RIVET_CLI_VERSION, "bin");
+ }
+
+ public static string GetRivetCLIPath()
+ {
+ return Path.Combine(GetBinDir(), "rivet");
+ }
+
+ public static Result RunRivetCLI(params string[] args)
+ {
+ // TODO: Turn this on if debug is enabled
+ // UnityEngine.Debug.Log($"Running Rivet CLI: {GetRivetCLIPath()} {string.Join(" ", args)}");
+
+ if (!File.Exists(GetRivetCLIPath()))
+ {
+ // TODO: Turn this on if debug is enabled
+ // UnityEngine.Debug.LogError("File does not exist: " + GetRivetCLIPath());
+ return new ErrorResult("File does not exist: " + GetRivetCLIPath());
+ }
+
+ var startInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = GetRivetCLIPath(),
+ Arguments = string.Join(" ", args),
+ RedirectStandardOutput = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+
+ try
+ {
+ using var process = System.Diagnostics.Process.Start(startInfo);
+ var output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit();
+
+ return new SuccessResult(JObject.Parse(output));
+ }
+ catch (System.Exception ex)
+ {
+ return new ErrorResult("Failed to start process: " + ex.Message);
+ }
+ }
+
+ public static Result Install()
+ {
+ string bin_dir = GetBinDir();
+
+ System.Environment.SetEnvironmentVariable("RIVET_CLI_VERSION", REQUIRED_RIVET_CLI_VERSION);
+ System.Environment.SetEnvironmentVariable("BIN_DIR", bin_dir);
+
+ var process = new System.Diagnostics.Process();
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.RedirectStandardOutput = true;
+
+ if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
+ {
+ process.StartInfo.FileName = "powershell.exe";
+ process.StartInfo.Arguments = "-Command \"iwr https://raw.githubusercontent.com/rivet-gg/cli/$env:RIVET_CLI_VERSION/install/windows.ps1 -useb | iex\"";
+ }
+ else
+ {
+ process.StartInfo.FileName = "/bin/sh";
+ process.StartInfo.Arguments = "-c \"curl -fsSL https://raw.githubusercontent.com/rivet-gg/cli/${RIVET_CLI_VERSION}/install/unix.sh | sh\"";
+ }
+
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ process.WaitForExit();
+
+ return new SuccessResult(output);
+ }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/RivetCLI.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetCLI.cs.meta
new file mode 100644
index 0000000..3f3aeeb
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetCLI.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 111d28c48b6bf457380774e98e995473
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/RivetPlugin.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetPlugin.cs
new file mode 100644
index 0000000..24b1adb
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetPlugin.cs
@@ -0,0 +1,140 @@
+using UnityEngine;
+using UnityEditor;
+
+namespace Rivet
+{
+ ///
+ /// Provides extension data for the Rivet plugin.
+ ///
+ public static class ExtensionData
+ {
+ private static string rivetToken;
+ private static string apiEndpoint = "https://api.rivet.gg";
+
+ ///
+ /// Gets or sets the Rivet token.
+ ///
+ ///
+ /// The Rivet token is used for authentication with the Rivet API.
+ /// When the token is set, it is also stored in the PlayerPrefs for persistence.
+ ///
+ public static string RivetToken
+ {
+ get { return rivetToken; }
+ set
+ {
+ rivetToken = value;
+ // This might not be called from the main thread, so we need to
+ // delay the call to PlayerPrefs
+ UnityEditor.EditorApplication.delayCall += () =>
+ {
+ PlayerPrefs.SetString("RivetToken", value);
+ };
+ }
+ }
+
+ ///
+ /// Gets or sets the API endpoint.
+ ///
+ ///
+ /// The API endpoint is the base URL for the Rivet API.
+ /// When the endpoint is set, it is also stored in the PlayerPrefs for persistence.
+ ///
+ public static string ApiEndpoint
+ {
+ get { return apiEndpoint; }
+ set
+ {
+ apiEndpoint = value;
+ // This might not be called from the main thread, so we need to
+ // delay the call to PlayerPrefs
+ UnityEditor.EditorApplication.delayCall += () =>
+ {
+ PlayerPrefs.SetString("ApiEndpoint", value);
+ };
+ }
+ }
+ }
+
+ namespace Rivet
+ {
+ public class RivetPluginWindow : EditorWindow
+ {
+ public string ApiEndpoint
+ {
+ get { return ExtensionData.ApiEndpoint; }
+ set { ExtensionData.ApiEndpoint = value; }
+ }
+
+ public string RivetToken
+ {
+ get { return ExtensionData.RivetToken; }
+ set { ExtensionData.RivetToken = value; }
+ }
+
+ // Define an interface for the states
+ public interface IState
+ {
+ void OnEnter(RivetPluginWindow pluginWindow);
+ void OnGUI();
+ }
+
+ [MenuItem("Window/Rivet Plugin")]
+ public static void ShowWindow()
+ {
+ GetWindow("Rivet Plugin");
+ }
+
+ // Add a variable to hold the current state
+ public IState currentState;
+
+ // Add a method to handle the state transitions
+ public void TransitionToState(IState newState)
+ {
+ currentState = newState;
+ currentState.OnEnter(this);
+ }
+
+ private Texture2D rivetLogo;
+
+ void OnGUI()
+ {
+ // Create an outer vertical container
+ GUILayout.BeginVertical();
+
+ // Draw the Rivet logo
+ if (rivetLogo != null)
+ {
+ GUILayout.Label(rivetLogo);
+ }
+ else
+ {
+ // Load the Rivet logo
+ rivetLogo = (Texture2D)AssetDatabase.LoadAssetAtPath("Assets/Rivet/Editor/Images/icon-text-white.png", typeof(Texture2D));
+ Debug.Log("Rivet logo not found");
+ }
+
+ // Draw the global Rivet buttons
+
+ // Links
+ GUILayout.BeginHorizontal();
+ if (GUILayout.Button("Hub")) Application.OpenURL("https://hub.rivet.gg/");
+ if (GUILayout.Button("Docs")) Application.OpenURL("https://rivet.gg/docs");
+ if (GUILayout.Button("Discord")) Application.OpenURL("https://rivet.gg/discord");
+ GUILayout.EndHorizontal();
+
+ // Call the OnGUI method of the current state
+ currentState.OnGUI();
+
+ // End the vertical container
+ GUILayout.EndVertical();
+ }
+
+ void OnEnable()
+ {
+ // Initialize the state machine
+ TransitionToState(new Installer());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/RivetPlugin.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetPlugin.cs.meta
new file mode 100644
index 0000000..6451381
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/RivetPlugin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6d2c369bd409046d288bb3600eaa5c79
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows.meta
new file mode 100644
index 0000000..a3314c0
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1ad51a974ce3f4d979ad6b27d2e252ba
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Installer.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Installer.cs
new file mode 100644
index 0000000..f17c5a5
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Installer.cs
@@ -0,0 +1,74 @@
+using UnityEngine;
+using UnityEditor;
+using Rivet.Rivet;
+
+namespace Rivet
+{
+ public class Installer : RivetPluginWindow.IState
+ {
+ public RivetPluginWindow window;
+ public string installLabelText;
+ public bool installButtonEnabled;
+
+ public Installer()
+ {
+ }
+
+ public void OnEnter(RivetPluginWindow pluginWindow)
+ {
+ this.window = pluginWindow;
+
+ // Prepare the installer
+ installLabelText = ReplacePlaceholders("%%version%% %%bin_dir%%");
+ installButtonEnabled = false;
+
+ // Start a new thread
+ new System.Threading.Thread(() =>
+ {
+ if (RivetCLI.CLIInstalled())
+ {
+ // Change to the Login screen
+ window.TransitionToState(new Login());
+ }
+ else
+ {
+ installButtonEnabled = true;
+ }
+ }).Start();
+ }
+
+ public void OnGUI()
+ {
+ // Code to run every frame in the Installer state
+ EditorGUILayout.LabelField(installLabelText);
+ EditorGUI.BeginDisabledGroup(!installButtonEnabled);
+ if (GUILayout.Button("Install"))
+ {
+ // Handle the Install button click
+ new System.Threading.Thread(() =>
+ {
+ var result = RivetCLI.Install();
+
+ switch (result)
+ {
+ case SuccessResult successResult:
+ // Change to the Login screen
+ window.TransitionToState(new Login());
+ break;
+ case ErrorResult errorResult:
+ // Debug the error
+ UnityEngine.Debug.LogError(errorResult.Message);
+ break;
+ }
+ }).Start();
+ }
+ EditorGUI.EndDisabledGroup();
+ }
+
+ public string ReplacePlaceholders(string text)
+ {
+ return text.Replace("%%version%%", RivetCLI.REQUIRED_RIVET_CLI_VERSION)
+ .Replace("%%bin_dir%%", RivetCLI.GetBinDir());
+ }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Installer.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Installer.cs.meta
new file mode 100644
index 0000000..5b1e341
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Installer.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5f0ad3329b8084d5f9a5539b2eef9fa4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Login.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Login.cs
new file mode 100644
index 0000000..5163efb
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Login.cs
@@ -0,0 +1,145 @@
+using UnityEngine;
+using UnityEditor;
+using Newtonsoft.Json.Linq;
+using Rivet.Rivet;
+
+namespace Rivet
+{
+ public class Login : RivetPluginWindow.IState
+ {
+ public RivetPluginWindow window;
+ public string url;
+ bool loginButtonEnabled = true;
+ private bool showAdvancedOptions = false;
+
+ public Login()
+ {
+ }
+
+ public void OnEnter(RivetPluginWindow pluginWindow)
+ {
+ // Initialize the URL
+ url = "";
+
+ this.window = pluginWindow;
+
+ // First, check if we're already logged in
+ var result = RivetCLI.RunCommand(
+ "sidekick",
+ "check-login-state");
+
+ switch (result)
+ {
+ case SuccessResult getLinkSuccessResult:
+ if (getLinkSuccessResult.Data["Ok"] == null)
+ {
+ // RivetPluginBridge.DisplayCliError(result); TODO:
+ UnityEngine.Debug.LogError("Error: " + result.Data);
+ loginButtonEnabled = true;
+ return;
+ }
+
+ window.TransitionToState(new Plugin());
+
+ break;
+ }
+ }
+
+ public void OnGUI()
+ {
+ UnityEditor.EditorGUI.BeginDisabledGroup(!loginButtonEnabled);
+
+ if (GUILayout.Button("Sign in to Rivet"))
+ {
+ new System.Threading.Thread(() =>
+ {
+ // Disable the button sign in button
+ loginButtonEnabled = false;
+
+ var getLinkResult = RivetCLI.RunCommand(
+ "--api-endpoint",
+ window.ApiEndpoint,
+ "sidekick",
+ "get-link");
+
+ switch (getLinkResult)
+ {
+ case SuccessResult getLinkSuccessResult:
+ // Verify that the version that came back is correct
+ if (getLinkSuccessResult.Data["Ok"] == null)
+ {
+ // RivetPluginBridge.DisplayCliError(result); TODO:
+ UnityEngine.Debug.LogError("Error: " + getLinkResult.Data);
+ loginButtonEnabled = true;
+ return;
+ }
+
+ var data = getLinkSuccessResult.Data["Ok"];
+
+ // Now that we have the link, open it in the user's browser
+ EditorApplication.delayCall += () =>
+ {
+ Application.OpenURL(data["device_link_url"].ToString());
+ };
+
+ // Long-poll the Rivet API until the user has logged in
+ var waitForLoginResult = RivetCLI.RunCommand(
+ "--api-endpoint",
+ window.ApiEndpoint,
+ "sidekick",
+ "wait-for-login",
+ "--device-link-token",
+ data["device_link_token"].ToString());
+
+ switch (getLinkResult)
+ {
+ case SuccessResult waitForLoginSuccessResult:
+ if (waitForLoginSuccessResult.Data["Ok"] == null)
+ {
+ // RivetPluginBridge.DisplayCliError(result); TODO:
+ UnityEngine.Debug.LogError("Error: " + getLinkResult.Data);
+ loginButtonEnabled = true;
+ return;
+ }
+
+ window.TransitionToState(new Plugin());
+
+ break;
+
+ case ErrorResult waitForLoginErrorResult:
+ // RivetPluginBridge.DisplayCliError(result); TODO:
+ UnityEngine.Debug.LogError("Error: " + getLinkResult.Data);
+ loginButtonEnabled = true;
+ return;
+
+ }
+
+ break;
+
+ case ErrorResult errorResult:
+ // RivetPluginBridge.DisplayCliError(result); TODO:
+ UnityEngine.Debug.LogError("Error: " + getLinkResult.Data);
+ loginButtonEnabled = true;
+ return;
+ default:
+ UnityEngine.Debug.LogError("Error: " + getLinkResult.Data);
+ loginButtonEnabled = true;
+ return;
+ }
+ }).Start();
+ }
+
+ UnityEditor.EditorGUI.EndDisabledGroup();
+
+ // Display the Advanced Options dropdown
+ showAdvancedOptions = EditorGUILayout.Foldout(showAdvancedOptions, "Advanced Options");
+
+ if (showAdvancedOptions)
+ {
+ // Display the API Endpoint text box
+ EditorGUILayout.LabelField("API Endpoint:");
+ window.ApiEndpoint = EditorGUILayout.TextField(window.ApiEndpoint);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Login.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Login.cs.meta
new file mode 100644
index 0000000..693cd8f
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Login.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 842ef04ebec2947d2ac7251e1d94731d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Plugin.cs b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Plugin.cs
new file mode 100644
index 0000000..d913b12
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Plugin.cs
@@ -0,0 +1,377 @@
+using UnityEngine;
+using UnityEditor;
+using Newtonsoft.Json.Linq;
+using System.Linq;
+using Newtonsoft.Json;
+using System.Net.Http;
+using System.Net;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.IO;
+using Rivet.Rivet;
+
+namespace Rivet
+{
+ public struct BootstrapData
+ {
+ public string ApiEndpoint
+ {
+ get { return ExtensionData.ApiEndpoint; }
+ set { ExtensionData.ApiEndpoint = value; }
+ }
+
+ [JsonProperty("game_id")] public string GameId;
+ [JsonProperty("token")] public string CloudToken;
+ }
+
+ public class Region
+ {
+ public string provider;
+ public string provider_display_name;
+ public string region_display_name;
+ public string region_id;
+ public string region_name_id;
+ public string universal_region;
+ }
+
+ public class Namespace
+ {
+ public string create_ts;
+ public string display_name;
+ public string name_id;
+ public string namespace_id;
+ public string version_id;
+ }
+
+ public class Version
+ {
+ public string create_ts;
+ public string display_name;
+ public string version_id;
+ }
+
+ public class Game
+ {
+ public List available_regions;
+ public string create_ts;
+ public string developer_group_id;
+ public string display_name;
+ public string game_id;
+ public string name_id;
+ public List namespaces;
+ public int total_player_count;
+ public List versions;
+ }
+
+ public class Watch
+ {
+ public string index;
+ }
+
+ public class Root
+ {
+ public Game game;
+ public Watch watch;
+ }
+
+ public class GameData
+ {
+ public List<(Namespace, Version)> namespaces;
+ }
+
+
+ public class Plugin : RivetPluginWindow.IState
+ {
+ public Texture logoTexture; // Assign this in the Unity editor
+ public GameData gameData;
+ public int selectedIndex = 0;
+ public bool showPlaytestOptions = true;
+ public bool showDeployOptions = false;
+ public bool showSettingsOptions = false;
+ public RivetPluginWindow window;
+ public BootstrapData bootstrapData;
+ private bool thisMachineSelected = false;
+
+ public void OnEnter(RivetPluginWindow pluginWindow)
+ {
+ this.window = pluginWindow;
+ new System.Threading.Thread(() =>
+ {
+ GetBootstrapData();
+ GetNamespaceToken();
+ }).Start();
+ }
+
+ private void GetBootstrapData()
+ {
+ var getBootstrapResult = RivetCLI.RunCommand(
+ "sidekick",
+ "get-bootstrap-data"
+ );
+
+ switch (getBootstrapResult)
+ {
+ case SuccessResult successResult:
+ var data = successResult.Data["Ok"];
+ // TODO: Deserialize this better
+ bootstrapData = new BootstrapData
+ {
+ ApiEndpoint = data["api_endpoint"].ToString(),
+ GameId = data["game_id"].ToString(),
+ CloudToken = data["token"].ToString()
+ };
+
+ // Update namespaces
+ FetchPluginData();
+
+ break;
+ case ErrorResult errorResult:
+ UnityEngine.Debug.LogError(errorResult.Message);
+ break;
+ }
+ }
+
+ public void FetchPluginData()
+ {
+ using var httpClient = new HttpClient();
+ var request = new HttpRequestMessage(HttpMethod.Get, $"{bootstrapData.ApiEndpoint}/cloud/games/{bootstrapData.GameId}");
+ request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", bootstrapData.CloudToken);
+ var response = httpClient.SendAsync(request).Result;
+
+ if (response.StatusCode != HttpStatusCode.OK)
+ {
+ Debug.LogError("Failed to fetch plugin data");
+ return;
+ }
+
+ var responseBody = response.Content.ReadAsStringAsync().Result;
+ var root = JsonConvert.DeserializeObject(responseBody);
+
+ var newGameData = new GameData
+ {
+ namespaces = new List<(Namespace, Version)>()
+ };
+
+ foreach (var ns in root.game.namespaces)
+ {
+ var version = root.game.versions.FirstOrDefault(version => version.version_id == ns.version_id);
+
+ if (version != null)
+ {
+ newGameData.namespaces.Add((ns, version));
+ }
+ }
+
+ // Return the data to the main thread
+ gameData = newGameData;
+ }
+
+ public void OnGUI()
+ {
+ GUILayout.BeginVertical();
+
+ try
+ {
+ // Logo
+ GUILayout.Label(logoTexture, GUILayout.Width(200), GUILayout.Height(200));
+
+ // Horizontal line
+ // GUILayout.Label("", GUI.skin.horizontalSlider);
+
+ // Buttons
+ GUILayout.BeginHorizontal();
+ if (GUILayout.Button("Playtest"))
+ {
+ showPlaytestOptions = true;
+ showDeployOptions = false;
+ showSettingsOptions = false;
+ new System.Threading.Thread(() =>
+ {
+ FetchPluginData();
+ }).Start();
+ }
+
+ if (GUILayout.Button("Deploy"))
+ {
+ showDeployOptions = true;
+ showPlaytestOptions = false;
+ showSettingsOptions = false;
+ new System.Threading.Thread(() =>
+ {
+ FetchPluginData();
+ }).Start();
+ }
+
+ if (GUILayout.Button("Settings")) { showSettingsOptions = true; showDeployOptions = false; showPlaytestOptions = false; }
+ GUILayout.EndHorizontal();
+
+ GUILayout.Space(40);
+
+ // Playtest options
+ if (showPlaytestOptions)
+ {
+ GUILayout.Label("Server");
+ GUIStyle buttonStyle = new(GUI.skin.button);
+ GUIStyle toggleButtonStyle = new(GUI.skin.button) { normal = GUI.skin.button.active };
+
+ GUILayout.BeginHorizontal();
+ bool thisMachineClicked = GUILayout.Button("This machine", thisMachineSelected ? toggleButtonStyle : buttonStyle);
+ if (thisMachineClicked) thisMachineSelected = true;
+
+ bool rivetServersClicked = GUILayout.Button("Rivet servers", !thisMachineSelected ? toggleButtonStyle : buttonStyle);
+ if (rivetServersClicked) thisMachineSelected = false;
+ GUILayout.EndHorizontal();
+
+ GUILayout.Space(20);
+
+ GUILayout.Label("Namespace");
+ if (gameData == null)
+ {
+ GUILayout.Label("Loading...");
+ }
+ else
+ {
+ var namespaces = gameData.namespaces.Select(space => space.Item1.display_name).ToArray();
+ if (namespaces.Length == 0)
+ {
+ GUILayout.Label("No namespaces found");
+ }
+ int oldSelectedIndex = selectedIndex;
+ selectedIndex = EditorGUILayout.Popup(selectedIndex, namespaces);
+
+ if (thisMachineClicked || rivetServersClicked || oldSelectedIndex != selectedIndex)
+ {
+ new System.Threading.Thread(() =>
+ {
+ GetNamespaceToken();
+ }).Start();
+ }
+ }
+ }
+
+ // Deploy options
+ if (showDeployOptions)
+ {
+ GUILayout.Label("Build and deploy server");
+ if (gameData == null)
+ {
+ GUILayout.Label("Loading...");
+ }
+ else
+ {
+ var namespaces = gameData.namespaces.Select(space => space.Item1.display_name).ToArray();
+ if (namespaces.Length == 0)
+ {
+ GUILayout.Label("No namespaces found");
+ }
+ selectedIndex = EditorGUILayout.Popup(selectedIndex, namespaces);
+ }
+
+ if (GUILayout.Button("Build and Deploy"))
+ {
+ // Update the game token
+ GetNamespaceToken();
+
+ // Check if Linux build support is installed
+ if (!BuildPipeline.IsBuildTargetSupported(BuildTargetGroup.Standalone, BuildTarget.StandaloneLinux64))
+ {
+ Debug.LogError("Linux build support is not installed");
+ return;
+ }
+
+ // Set the build settings
+ BuildPlayerOptions buildPlayerOptions = new BuildPlayerOptions
+ {
+ scenes = EditorBuildSettings.scenes.Select(scene => scene.path).ToArray(),
+ locationPathName = "builds/LinuxServer/LinuxServer.x86_64", // Output path
+ target = BuildTarget.StandaloneLinux64, // Target platform
+ subtarget = (int)StandaloneBuildSubtarget.Server // Headless mode for server build
+ };
+
+ // Build the player
+ var result = BuildPipeline.BuildPlayer(buildPlayerOptions);
+
+ // If the build failed, log an error, and don't continue
+ if (result.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded)
+ {
+ Debug.LogError("Build failed: " + result.summary.result);
+ return;
+ }
+
+ // Run deploy with CLI
+ new System.Threading.Thread(() =>
+ {
+ var result = RivetCLI.RunCommand(
+ "sidekick",
+ "--show-terminal",
+ "deploy",
+ "--namespace",
+ gameData.namespaces[selectedIndex].Item1.name_id
+ );
+ }).Start();
+
+ return;
+ }
+ }
+
+ // Settings options
+ if (showSettingsOptions)
+ {
+ if (GUILayout.Button("Unlink game"))
+ {
+ new System.Threading.Thread(() =>
+ {
+ // First, check if we're already logged in
+ var result = RivetCLI.RunCommand(
+ "unlink");
+
+ switch (result)
+ {
+ case SuccessResult getLinkSuccessResult:
+ if (getLinkSuccessResult.Data["Ok"] == null)
+ {
+ UnityEngine.Debug.LogError("Error: " + result.Data);
+ return;
+ }
+
+ break;
+ }
+
+ window.TransitionToState(new Login());
+ }).Start();
+ };
+ }
+ }
+ finally
+ {
+ GUILayout.EndVertical();
+ }
+ }
+
+ ///
+ /// Retrieves the namespace token based on the selected machine and namespace ID.
+ ///
+ private void GetNamespaceToken()
+ {
+ var command = thisMachineSelected ? "get-namespace-development-token" : "get-namespace-public-token";
+ var namespaceId = gameData.namespaces[selectedIndex].Item1.name_id;
+
+ var result = RivetCLI.RunCommand("sidekick", command, "--namespace", namespaceId);
+
+ switch (result)
+ {
+ case SuccessResult successResult:
+ var token = successResult.Data["Ok"]["token"].ToString();
+ window.RivetToken = token;
+ UnityEditor.EditorApplication.delayCall += () =>
+ {
+ PlayerPrefs.SetString("RIVET_EDITOR_TOKEN", token);
+ };
+ break;
+ case ErrorResult errorResult:
+ UnityEngine.Debug.LogError(errorResult.Message);
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Plugin.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Plugin.cs.meta
new file mode 100644
index 0000000..275eec8
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Editor/Windows/Plugin.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: eab40917f63af4e008a47dd59a7d6693
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/README.md b/unity/tanks-fishnet/Assets/Rivet/README.md
new file mode 100644
index 0000000..85430b3
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/README.md
@@ -0,0 +1,93 @@
+# Rivet Unity Plugin
+
+The Rivet Unity Plugin allows you to connect your game with Rivet, and easily
+build and ship your multiplayer games to the world in under a minute.
+
+More learning content related to Unity and Rivet can be found in the [Rivet
+Learning Center](https://rivet.gg/learn/unity). You can also find an example of
+this plugin being used in the [Rivet examples
+repository](https://github.com/rivet-gg/examples/tree/main/unity/tanks-fishnet).
+
+# 1.0 Table of Contents
+
+- [Rivet Unity Plugin](#rivet-unity-plugin)
+- [1.0 Table of Contents](#10-table-of-contents)
+- [2.0 Plugin Usage](#20-plugin-usage)
+ - [2.1 Linking](#21-linking)
+ - [2.2 Playtest Tab](#22-playtest-tab)
+ - [2.3 Deploy Tab](#23-deploy-tab)
+ - [2.4 Settings Tab](#24-settings-tab)
+ - [2.5 API Usage and Reference](#25-api-usage-and-reference)
+
+# 2.0 Plugin Usage
+
+## 2.1 Linking
+
+First, you'll need to link the Rivet Unity Plugin to your project on the Rivet
+Hub. You can open the Rivet Unity Plugin window by going to `Window > Rivet
+Plugin`.
+
+At this point, if the Rivet CLI isn't installed, the plugin shows a button that
+handles the installation for you.
+
+After the installation, you'll be prompted to link your project with the Rivet
+Hub. By clicking "Sign in with Rivet", a browser window will be opened and let
+you select or create a project on the Rivet Hub.
+
+## 2.2 Playtest Tab
+
+The playtesting tab allows you to select how your local game and game builds
+will connect to Rivet servers. There are two items you can change here:
+
+- **Server**: This allows you to select if you want to connect to a locally
+ hosted server with `This machine`, or Rivet servers with `Rivet servers`. More
+ often, you'll likely want to use Rivet servers so that you can quickly get
+ others testing the game with publically accessible lobbies. Local testing can
+ allow you to run a server on your machine for better debugging purposes.
+- **Namespace**: This will allow you to choose which namespace you'd like to
+ connect to. By default, the options are "Production" and "Staging", but others
+ can be added on the Rivet Hub.
+
+## 2.3 Deploy Tab
+
+The Deploy tab allows you to build your game server and upload it to Rivet. You
+can choose which namespace you'd like to deploy to. By default, the options are
+"Production" and "Staging".
+
+Staging can be used for testing, or to playtest with others. After you upload to
+staging, make sure to go back to the Playtest tab and select the "Staging"
+namespace, and change the server to "Rivet Servers".
+
+Once you're ready to deploy to production, you can select the "Production"
+namespace and deploy your server there. As soon as you deploy to production,
+your updated server will be live for any future lobbies that are started.
+
+## 2.4 Settings Tab
+
+From the settings tab, you may unlink your game. This will bring you back to the
+login screen, allowing you to re-link your project if needed.
+
+## 2.5 API Usage and Reference
+
+You can find the full Rivet API reference
+[here](https://rivet.gg/docs/matchmaker). Items under the Matchmaker API
+(`lobbies`, `players`, and `regions`) are all implemented in the Unity plugin.
+Here's how you might use the `matchmaker/lobbies/find` endpoint:
+
+```csharp
+// Find lobby and connect with FishNet
+var rivetManager = FindObjectOfType();
+StartCoroutine(rivetManager.FindLobby(new FindLobbyRequest
+{
+ GameModes = new[] { gameMode },
+}, res =>
+{
+ // Connect to server
+ var port = res.Ports["default"];
+ Debug.Log("Connecting to " + port.Hostname + ":" + port.Port);
+ var networkManager = FindObjectOfType();
+ networkManager.ClientManager.StartConnection(port.Hostname, port.Port);
+
+ UpdateConnectionInfo();
+}, fail => { Debug.Log($"Failed to find lobby: {fail}"); }));
+```
diff --git a/unity/tanks-fishnet/Assets/Rivet/README.md.meta b/unity/tanks-fishnet/Assets/Rivet/README.md.meta
new file mode 100644
index 0000000..b8c5eec
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/README.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: eaf19bb4428f643bc9f606eed1d55715
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Runtime.meta b/unity/tanks-fishnet/Assets/Rivet/Runtime.meta
new file mode 100644
index 0000000..0f1fc17
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ec74c9f56be464ac1997682685d4d5fd
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.cs b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.cs
new file mode 100644
index 0000000..898e05c
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.cs
@@ -0,0 +1,394 @@
+#nullable enable
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using UnityEngine;
+using UnityEngine.Networking;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+using System.IO;
+
+namespace Rivet
+{
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum CreateLobbyRequestPublicity
+ {
+ [EnumMember(Value = "public")] Public,
+ [EnumMember(Value = "private")] Private,
+ }
+
+ public struct FindLobbyRequest
+ {
+ [JsonProperty("game_modes")] public string[] GameModes;
+ [JsonProperty("regions")] public string[]? Regions;
+ }
+
+ public struct JoinLobbyRequest
+ {
+ [JsonProperty("lobby_id")] public string LobbyId;
+ }
+
+ public struct CreateLobbyRequest
+ {
+ [JsonProperty("game_mode")] public string GameMode;
+ [JsonProperty("region")] public string? Region;
+ [JsonProperty("publicity")] public CreateLobbyRequestPublicity Publicity;
+ [JsonProperty("lobby_config")] public JObject? LobbyConfig;
+ }
+
+ public struct FindLobbyResponse
+ {
+ [JsonProperty("lobby")] public RivetLobby Lobby;
+ [JsonProperty("ports")] public Dictionary Ports;
+ [JsonProperty("player")] public RivetPlayer Player;
+ }
+
+ public struct RivetLobby
+ {
+ [JsonProperty("lobby_id")] public string LobbyId;
+ [JsonProperty("host")] public string Host;
+ [JsonProperty("port")] public int Port;
+ }
+
+ public struct RivetLobbyPort
+ {
+ [JsonProperty("hostname")] public string? Hostname;
+ [JsonProperty("port")] public ushort Port;
+ [JsonProperty("is_tls")] public bool IsTls;
+ }
+
+ public struct RivetPlayer
+ {
+ [JsonProperty("token")] public string Token;
+ }
+
+ [System.Serializable]
+ public class RivetSettings
+ {
+ public string? RivetToken;
+ public string? ApiEndpoint;
+ }
+
+ public class RivetManager : MonoBehaviour
+ {
+ [HideInInspector]
+ public string? MatchmakerApiEndpoint => ApiEndpoint + "/matchmaker";
+
+ ///
+ /// The response from the last call. Used to maintain information about the Rivet player &
+ /// lobby.
+ ///
+ public FindLobbyResponse? FindLobbyResponse { get; private set; }
+
+ [HideInInspector]
+ public string? RivetToken => GetRivetToken();
+
+ [HideInInspector]
+ public string? ApiEndpoint => GetApiEndpoint();
+
+ private string? GetRivetToken()
+ {
+ string? token = PlayerPrefs.GetString("RivetToken");
+ if (string.IsNullOrEmpty(token))
+ {
+ var rivetSettings = LoadRivetSettings();
+ if (rivetSettings != null)
+ {
+ token = rivetSettings.RivetToken;
+ }
+ }
+ return token;
+ }
+
+ private string? GetApiEndpoint()
+ {
+ string? endpoint = PlayerPrefs.GetString("ApiEndpoint");
+ if (string.IsNullOrEmpty(endpoint))
+ {
+ var rivetSettings = LoadRivetSettings();
+ if (rivetSettings != null)
+ {
+ endpoint = rivetSettings.ApiEndpoint;
+ }
+ }
+ return endpoint;
+ }
+
+ private RivetSettings? LoadRivetSettings()
+ {
+ string filePath = Path.Combine(Application.streamingAssetsPath, "rivet_export.json");
+ if (File.Exists(filePath))
+ {
+ string json = File.ReadAllText(filePath);
+ RivetSettings rivetSettings = JsonUtility.FromJson(json);
+ return rivetSettings;
+ }
+ else
+ {
+ Debug.LogError("File not found: " + filePath);
+ return null;
+ }
+ }
+
+ // Start function that debugs the Rivet token and API endpoint
+ private void Start()
+ {
+ Debug.Log("Rivet Token: " + RivetToken);
+ Debug.Log("API Endpoint: " + ApiEndpoint);
+ }
+
+ #region API: Matchmaker.Lobbies
+
+ ///
+ /// Documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerator FindLobby(FindLobbyRequest request, Action success,
+ Action fail)
+ {
+ yield return PostRequest(MatchmakerApiEndpoint + "/lobbies/find",
+ request, res =>
+ {
+ // Save response
+ FindLobbyResponse = res;
+ success(res);
+ }, fail);
+ }
+
+ ///
+ /// Documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerator JoinLobby(JoinLobbyRequest request, Action success,
+ Action fail)
+ {
+ yield return PostRequest(MatchmakerApiEndpoint + "/lobbies/join",
+ request, res =>
+ {
+ // Save response
+ FindLobbyResponse = res;
+ success(res);
+ }, fail);
+ }
+
+ ///
+ /// Documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerator CreateLobby(CreateLobbyRequest request, Action success,
+ Action fail)
+ {
+ yield return PostRequest(MatchmakerApiEndpoint + "/lobbies/create",
+ request, res =>
+ {
+ // Save response
+ FindLobbyResponse = res;
+ success(res);
+ }, fail);
+ }
+
+ ///
+ /// Documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerator LobbyReady(Action success, Action fail)
+ {
+ yield return PostRequest, object>(MatchmakerApiEndpoint + "/lobbies/ready",
+ new Dictionary(), (_) => success(), fail);
+ }
+
+ #endregion
+
+ #region API: Matchmaker.Players
+
+ ///
+ /// Documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerator PlayerConnected(string playerToken, Action success, Action fail)
+ {
+ yield return PostRequest, object>(
+ MatchmakerApiEndpoint + "/players/connected",
+ new Dictionary
+ {
+ { "player_token", playerToken },
+ }, (_) => success(), fail);
+ }
+
+ ///
+ /// Documentation
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerator PlayerDisconnected(string playerToken, Action success, Action fail)
+ {
+ yield return PostRequest, object>(
+ MatchmakerApiEndpoint + "/players/disconnected", new Dictionary
+ {
+ { "player_token", playerToken },
+ }, (_) => success(), fail);
+ }
+
+ #endregion
+
+ #region API Requests
+
+ private string GetToken()
+ {
+ // Try loading from environment
+ var token = Environment.GetEnvironmentVariable("RIVET_TOKEN");
+ if (token != null && token.Length > 0)
+ {
+ return token;
+ }
+
+ // Try loading from PlayerPrefs
+ string value = PlayerPrefs.GetString("RIVET_EDITOR_TOKEN");
+ if (value.Length > 0)
+ {
+ return value;
+ }
+
+ // Try loading from RivetSettings
+ if (RivetToken != null && RivetToken.Length > 0)
+ {
+ return RivetToken;
+ }
+
+ throw new Exception("RIVET_TOKEN not set");
+ }
+
+ public IEnumerator PostRequest(string url, TReq requestBody, Action success, Action fail, string token = "")
+ {
+ if (token.Length == 0)
+ {
+ token = GetToken();
+ }
+
+ var debugRequestDescription = "POST " + url;
+
+ var requestBodyStr = JsonConvert.SerializeObject(requestBody,
+ new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
+
+ var www = UnityWebRequest.Post(url, requestBodyStr, "application/json");
+ www.SetRequestHeader("Authorization", "Bearer " + token);
+
+ yield return www.SendWebRequest();
+
+ switch (www.result)
+ {
+ case UnityWebRequest.Result.InProgress:
+ break;
+ case UnityWebRequest.Result.Success:
+ if (www.responseCode == 200)
+ {
+ var responseBody = JsonConvert.DeserializeObject(www.downloadHandler.text);
+ success(responseBody!);
+ }
+ else
+ {
+ string statusError = "Error status " + www.responseCode + ": " + www.downloadHandler.text;
+ Debug.LogError(debugRequestDescription + " " + statusError);
+ fail(statusError);
+ }
+
+ break;
+ case UnityWebRequest.Result.ConnectionError:
+ string connectionError = "ConnectionError: " + www.error;
+ Debug.LogError(debugRequestDescription + " " + connectionError + "\n" + Environment.StackTrace);
+ fail(connectionError);
+ break;
+ case UnityWebRequest.Result.ProtocolError:
+ string protocolError = "ProtocolError: " + www.error + " " + www.downloadHandler.text;
+ Debug.LogError(debugRequestDescription + " " + protocolError + "\n" + Environment.StackTrace);
+ fail(protocolError);
+ break;
+ case UnityWebRequest.Result.DataProcessingError:
+ string dpe = "DataProcessingError: " + www.error;
+ Debug.LogError(debugRequestDescription + " " + dpe + "\n" + Environment.StackTrace);
+ fail(dpe);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ public IEnumerator GetRequest(string url, Action success, Action fail, string token = "")
+ {
+ if (token.Length == 0)
+ {
+ token = GetToken();
+ }
+
+ var debugRequestDescription = "GET " + url;
+
+ var www = UnityWebRequest.Get(url);
+ www.SetRequestHeader("Authorization", "Bearer " + GetToken());
+
+ yield return www.SendWebRequest();
+
+ switch (www.result)
+ {
+ case UnityWebRequest.Result.InProgress:
+ break;
+ case UnityWebRequest.Result.Success:
+ if (www.responseCode == 200)
+ {
+ var responseBody = JsonConvert.DeserializeObject(www.downloadHandler.text);
+ success(responseBody!);
+ }
+ else
+ {
+ string statusError = "Error status " + www.responseCode + ": " + www.downloadHandler.text;
+ Debug.LogError(debugRequestDescription + " " + statusError);
+ fail(statusError);
+ }
+
+ break;
+ case UnityWebRequest.Result.ConnectionError:
+ string connectionError = "ConnectionError: " + www.error;
+ Debug.LogError(debugRequestDescription + " " + connectionError + "\n" + Environment.StackTrace);
+ fail(connectionError);
+ break;
+ case UnityWebRequest.Result.ProtocolError:
+ string protocolError = "ProtocolError: " + www.error + " " + www.downloadHandler.text;
+ Debug.LogError(debugRequestDescription + " " + protocolError + "\n" + Environment.StackTrace);
+ fail(protocolError);
+ break;
+ case UnityWebRequest.Result.DataProcessingError:
+ string dpe = "DataProcessingError: " + www.error;
+ Debug.LogError(debugRequestDescription + " " + dpe + "\n" + Environment.StackTrace);
+ fail(dpe);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Scripts/RivetManager.cs.meta b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.cs.meta
similarity index 83%
rename from unity/tanks-fishnet/Assets/Scripts/RivetManager.cs.meta
rename to unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.cs.meta
index d05f4b4..955eb8e 100644
--- a/unity/tanks-fishnet/Assets/Scripts/RivetManager.cs.meta
+++ b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 3781f297e673b0940943fedd28d18ef2
+guid: 968401380e77644ab8c094fa37084533
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.prefab b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.prefab
new file mode 100644
index 0000000..405bf28
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.prefab
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d9f5631b2574644275381a81a796a8ea79dc3c48bf57fb9d45c3571b48a9dd27
+size 1372
diff --git a/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.prefab.meta b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.prefab.meta
new file mode 100644
index 0000000..9acc544
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/Runtime/RivetManager.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 1757080393b12a4fda212c0494ff52f1
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Rivet/package.json b/unity/tanks-fishnet/Assets/Rivet/package.json
new file mode 100644
index 0000000..d990de3
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "com.rivet.rivet",
+ "displayName": "Rivet",
+ "version": "24.0.0",
+ "unity": "2021.3",
+ "description": "Description of the Rivet package",
+ "keywords": ["rivet", "unity", "package"],
+ "author": {
+ "name": "Forest Anderson",
+ "email": "forest@rivet.gg",
+ "url": "https://rivet.gg"
+ },
+ "dependencies": {
+ "com.unity.nuget.newtonsoft-json": "2.0.2"
+ }
+}
diff --git a/unity/tanks-fishnet/Assets/Rivet/package.json.meta b/unity/tanks-fishnet/Assets/Rivet/package.json.meta
new file mode 100644
index 0000000..4d7c8fd
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Rivet/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0044b76306aa49a93a9b09e523a3a59d
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/Scenes/Main.unity b/unity/tanks-fishnet/Assets/Scenes/Main.unity
index cef6b68..5c8f384 100644
--- a/unity/tanks-fishnet/Assets/Scenes/Main.unity
+++ b/unity/tanks-fishnet/Assets/Scenes/Main.unity
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c814d24c202e78e7dfd356d7b23210392fee79dbe6624eb5bf4d0bb8ddf1663f
-size 142302
+oid sha256:3d35461be48c919fed708ead7fec1fbf06438c29a26789533e85cd1aa1ffb717
+size 142452
diff --git a/unity/tanks-fishnet/Assets/Scripts/Player.cs b/unity/tanks-fishnet/Assets/Scripts/Player.cs
index 8b99e11..9c6efee 100644
--- a/unity/tanks-fishnet/Assets/Scripts/Player.cs
+++ b/unity/tanks-fishnet/Assets/Scripts/Player.cs
@@ -1,5 +1,3 @@
-using System.Collections;
-using System.Collections.Generic;
using FishNet.Managing.Logging;
using UnityEngine;
using FishNet.Object;
diff --git a/unity/tanks-fishnet/Assets/Scripts/RivetAuthenticator.cs b/unity/tanks-fishnet/Assets/Scripts/RivetAuthenticator.cs
index a93ac1c..b95008e 100644
--- a/unity/tanks-fishnet/Assets/Scripts/RivetAuthenticator.cs
+++ b/unity/tanks-fishnet/Assets/Scripts/RivetAuthenticator.cs
@@ -5,7 +5,7 @@
using FishNet.Connection;
using FishNet.Managing;
using FishNet.Transporting;
-using Unity.VisualScripting.Antlr3.Runtime;
+using Rivet;
using UnityEngine;
public class RivetAuthenticator : Authenticator
diff --git a/unity/tanks-fishnet/Assets/Scripts/RivetManager.cs b/unity/tanks-fishnet/Assets/Scripts/RivetManager.cs
deleted file mode 100644
index 66b5b69..0000000
--- a/unity/tanks-fishnet/Assets/Scripts/RivetManager.cs
+++ /dev/null
@@ -1,386 +0,0 @@
-#nullable enable
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Runtime.Serialization;
-using FishNet.Managing;
-using FishNet.Transporting;
-using UnityEngine;
-using UnityEngine.Networking;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-using Newtonsoft.Json.Linq;
-using UnityEngine.Serialization;
-
-[JsonConverter(typeof(StringEnumConverter))]
-public enum CreateLobbyRequestPublicity
-{
- [EnumMember(Value = "public")] Public,
- [EnumMember(Value = "private")] Private,
-}
-
-public struct FindLobbyRequest
-{
- [JsonProperty("game_modes")] public string[] GameModes;
- [JsonProperty("regions")] public string[]? Regions;
-}
-
-public struct JoinLobbyRequest
-{
- [JsonProperty("lobby_id")] public string LobbyId;
-}
-
-public struct CreateLobbyRequest
-{
- [JsonProperty("game_mode")] public string GameMode;
- [JsonProperty("region")] public string? Region;
- [JsonProperty("publicity")] public CreateLobbyRequestPublicity Publicity;
- [JsonProperty("lobby_config")] public JObject? LobbyConfig;
-}
-
-public struct FindLobbyResponse
-{
- [JsonProperty("lobby")] public RivetLobby Lobby;
- [JsonProperty("ports")] public Dictionary Ports;
- [JsonProperty("player")] public RivetPlayer Player;
-}
-
-public struct RivetLobby
-{
- [JsonProperty("lobby_id")] public string LobbyId;
- [JsonProperty("host")] public string Host;
- [JsonProperty("port")] public int Port;
-}
-
-public struct RivetLobbyPort
-{
- [JsonProperty("hostname")] public string? Hostname;
- [JsonProperty("port")] public ushort Port;
- [JsonProperty("is_tls")] public bool IsTls;
-}
-
-public struct RivetPlayer
-{
- [JsonProperty("token")] public string Token;
-}
-
-public class RivetManager : MonoBehaviour
-{
- const string MatchmakerApiEndpoint = "https://api.staging2.gameinc.io/matchmaker";
-
- public string? rivetToken = null;
-
- ///
- /// The game mode to start the game with.
- ///
- /// Only available on the server.
- ///
- [HideInInspector] public string? gameModeName = null;
-
- ///
- /// The lobby config provided for a custom lobby.
- ///
- private string? _lobbyConfigRaw = null;
-
- // Parse LobbyConfigRaw to JObject
- public JObject? LobbyConfig => _lobbyConfigRaw != null ? JObject.Parse(_lobbyConfigRaw) : null;
-
- #region References
-
- private NetworkManager _networkManager = null!;
- private RivetAuthenticator _authenticator = null!;
-
- #endregion
-
- ///
- /// The response from the last call. Used to maintain information about the Rivet player &
- /// lobby.
- ///
- public FindLobbyResponse? FindLobbyResponse { get; private set; }
-
- private void Start()
- {
- _networkManager = FindObjectOfType();
-
- // Configure client authentication
- _networkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState;
- _networkManager.ClientManager.RegisterBroadcast(OnTokenResponseBroadcast);
-
- // Start server if testing in editor or running from CLI
- if ((Application.isEditor && GetToken().StartsWith("dev_")) || Application.isBatchMode)
- {
- StartServer();
- }
- }
-
-
- #region Server
-
- private void StartServer()
- {
- Debug.Log("Starting server on port " + GetServerPort());
-
- // Read environment variables
- gameModeName = Environment.GetEnvironmentVariable("RIVET_GAME_MODE_NAME");
- _lobbyConfigRaw = Environment.GetEnvironmentVariable("RIVET_LOBBY_CONFIG");
-
- // Start server
- _networkManager.TransportManager.Transport.SetServerBindAddress("0.0.0.0", IPAddressType.IPv4);
- _networkManager.TransportManager.Transport.SetPort(GetServerPort());
- _networkManager.ServerManager.StartConnection();
- _networkManager.ServerManager.OnRemoteConnectionState += (conn, args) =>
- {
- Debug.Log("Remote connection state: " + conn.ClientId + " " + conn.GetAddress() + " " + args.ConnectionState);
- };
-
- // Create authentication
- _authenticator = gameObject.AddComponent();
- _networkManager.ServerManager.SetAuthenticator(_authenticator);
-
- // Notify Rivet this server can start accepting players
- StartCoroutine(LobbyReady(() => { Debug.Log("Lobby ready"); }, _ => { }));
- }
-
- private ushort GetServerPort()
- {
- var port = Environment.GetEnvironmentVariable("PORT_default");
- return port != null ? ushort.Parse(port) : (ushort) 7770;
- }
-
- #endregion
-
- #region Authentication
-
- private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs args)
- {
- if (args.ConnectionState != LocalConnectionState.Started)
- return;
-
- // Send request
- var token = FindLobbyResponse?.Player.Token;
- Debug.Log("Sending authenticate token request: " + token);
- var pb = new RivetAuthenticator.TokenRequestBroadcast()
- {
- Token = token
- };
- _networkManager.ClientManager.Broadcast(pb);
- }
-
- private void OnTokenResponseBroadcast(RivetAuthenticator.TokenResponseBroadcast trb)
- {
- Debug.Log("Token response: " + trb.Valid);
- string result = (trb.Valid) ? "Token authenticated." : "Token authentication failed.";
- _networkManager.Log(result);
- }
-
- #endregion
-
- #region API: Matchmaker.Lobbies
-
- ///
- /// Documentation
- ///
- ///
- ///
- ///
- ///
- ///
- public IEnumerator FindLobby(FindLobbyRequest request, Action success,
- Action fail)
- {
- yield return PostRequest(MatchmakerApiEndpoint + "/lobbies/find",
- request, res =>
- {
- // Save response
- FindLobbyResponse = res;
-
- // Connect to server
- var port = res.Ports["default"];
- Debug.Log("Connecting to " + port.Hostname + ":" + port.Port);
- _networkManager.ClientManager.StartConnection(port.Hostname, port.Port);
-
- success(res);
- }, fail);
- }
-
- ///
- /// Documentation
- ///
- ///
- ///
- ///
- ///
- ///
- public IEnumerator JoinLobby(JoinLobbyRequest request, Action success,
- Action fail)
- {
- yield return PostRequest(MatchmakerApiEndpoint + "/lobbies/join",
- request, res =>
- {
- // Save response
- FindLobbyResponse = res;
-
- // Connect to server
- var port = res.Ports["default"];
- Debug.Log("Connecting to " + port.Hostname + ":" + port.Port);
- _networkManager.ClientManager.StartConnection(port.Hostname, port.Port);
-
- success(res);
- }, fail);
- }
-
- ///
- /// Documentation
- ///
- ///
- ///
- ///
- ///
- ///
- public IEnumerator CreateLobby(CreateLobbyRequest request, Action success,
- Action fail)
- {
- yield return PostRequest(MatchmakerApiEndpoint + "/lobbies/create",
- request, res =>
- {
- // Save response
- FindLobbyResponse = res;
-
- // Connect to server
- var port = res.Ports["default"];
- Debug.Log("Connecting to " + port.Hostname + ":" + port.Port);
- _networkManager.ClientManager.StartConnection(port.Hostname, port.Port);
-
- success(res);
- }, fail);
- }
-
- ///
- /// Documentation
- ///
- ///
- ///
- ///
- ///
- public IEnumerator LobbyReady(Action success, Action fail)
- {
- yield return PostRequest, object>(MatchmakerApiEndpoint + "/lobbies/ready",
- new Dictionary(), (_) => success(), fail);
- }
-
- #endregion
-
- #region API: Matchmaker.Players
-
- ///
- /// Documentation
- ///
- ///
- ///
- ///
- ///
- ///
- public IEnumerator PlayerConnected(string playerToken, Action success, Action fail)
- {
- yield return PostRequest, object>(
- MatchmakerApiEndpoint + "/players/connected",
- new Dictionary
- {
- { "player_token", playerToken },
- }, (_) => success(), fail);
- }
-
- ///
- /// Documentation
- ///
- ///
- ///
- ///
- ///
- ///
- public IEnumerator PlayerDisconnected(string playerToken, Action success, Action fail)
- {
- yield return PostRequest, object>(
- MatchmakerApiEndpoint + "/players/disconnected", new Dictionary
- {
- { "player_token", playerToken },
- }, (_) => success(), fail);
- }
-
- #endregion
-
- #region API Requests
-
- private string GetToken()
- {
- #if UNITY_SERVER
- var token = Environment.GetEnvironmentVariable("RIVET_TOKEN");
- if (token != null)
- {
- return token;
- }
- #endif
-
- if (rivetToken != null && rivetToken.Length > 0)
- {
- return rivetToken;
- }
-
- throw new Exception("RIVET_TOKEN not set");
- }
-
- private IEnumerator PostRequest(string url, TReq requestBody, Action success, Action fail)
- {
- var debugRequestDescription = "POST " + url;
-
- var requestBodyStr = JsonConvert.SerializeObject(requestBody,
- new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
- Debug.Log(debugRequestDescription + " Request: " + requestBodyStr + "\n" + Environment.StackTrace);
-
- var www = UnityWebRequest.Post(url, requestBodyStr, "application/json");
- www.SetRequestHeader("Authorization", "Bearer " + GetToken());
-
- yield return www.SendWebRequest();
-
- switch (www.result)
- {
- case UnityWebRequest.Result.InProgress:
- Debug.Log("In progress");
- break;
- case UnityWebRequest.Result.Success:
- if (www.responseCode == 200)
- {
- Debug.Log(debugRequestDescription + " Success: " + www.downloadHandler.text);
- var responseBody = JsonConvert.DeserializeObject(www.downloadHandler.text);
- success(responseBody!);
- }
- else
- {
- string statusError = "Error status " + www.responseCode + ": " + www.downloadHandler.text;
- Debug.LogError(debugRequestDescription + " " + statusError);
- fail(statusError);
- }
-
- break;
- case UnityWebRequest.Result.ConnectionError:
- string connectionError = "ConnectionError: " + www.error;
- Debug.LogError(debugRequestDescription + " " + connectionError + "\n" + Environment.StackTrace);
- fail(connectionError);
- break;
- case UnityWebRequest.Result.ProtocolError:
- string protocolError = "ProtocolError: " + www.error + " " + www.downloadHandler.text;
- Debug.LogError(debugRequestDescription + " " + protocolError + "\n" + Environment.StackTrace);
- fail(protocolError);
- break;
- case UnityWebRequest.Result.DataProcessingError:
- string dpe = "DataProcessingError: " + www.error;
- Debug.LogError(debugRequestDescription + " " + dpe + "\n" + Environment.StackTrace);
- fail(dpe);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/unity/tanks-fishnet/Assets/Scripts/RivetUI.cs b/unity/tanks-fishnet/Assets/Scripts/RivetUI.cs
index 0d22c3e..4b6aeaf 100644
--- a/unity/tanks-fishnet/Assets/Scripts/RivetUI.cs
+++ b/unity/tanks-fishnet/Assets/Scripts/RivetUI.cs
@@ -2,9 +2,9 @@
using FishNet.Managing;
using FishNet.Transporting;
using Newtonsoft.Json.Linq;
+using Rivet;
using TMPro;
using UnityEngine;
-using UnityEngine.Serialization;
using UnityEngine.UI;
public class RivetUI : MonoBehaviour
@@ -22,6 +22,7 @@ public class RivetUI : MonoBehaviour
private void Start()
{
+ Debug.Log("RivetU2I.Start");
_networkManager = FindObjectOfType();
_rivetManager = FindObjectOfType();
@@ -47,6 +48,7 @@ private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs obj
public void OnClick_Find(string gameMode)
{
+ Debug.Log("Finding lobby...");
// Hide menu
joinMenuPanel.SetActive(false);
@@ -54,9 +56,17 @@ public void OnClick_Find(string gameMode)
StartCoroutine(_rivetManager.FindLobby(new FindLobbyRequest
{
GameModes = new[] { gameMode },
- }, _ => UpdateConnectionInfo(), fail => { Debug.Log($"Failed to find lobby: {fail}"); }));
+ }, res =>
+ {
+ // Connect to server
+ var port = res.Ports["default"];
+ Debug.Log("Connecting to " + port.Hostname + ":" + port.Port);
+ _networkManager.ClientManager.StartConnection(port.Hostname, port.Port);
+
+ UpdateConnectionInfo();
+ }, fail => { Debug.Log($"Failed to find lobby: {fail}"); }));
}
-
+
public void OnClick_Join()
{
// Hide menu
@@ -68,7 +78,7 @@ public void OnClick_Join()
LobbyId = lobbyIdInputField.text,
}, _ => UpdateConnectionInfo(), fail => { Debug.Log($"Failed to join lobby: {fail}"); }));
}
-
+
public void OnClick_Create()
{
// Hide menu
@@ -84,7 +94,7 @@ public void OnClick_Create()
},
}, _ => UpdateConnectionInfo(), fail => { Debug.Log($"Failed to create lobby: {fail}"); }));
}
-
+
public void OnClick_CopyLobbyId()
{
GUIUtility.systemCopyBuffer = _rivetManager.FindLobbyResponse?.Lobby.LobbyId ?? "";
diff --git a/unity/tanks-fishnet/Assets/Scripts/Server.cs b/unity/tanks-fishnet/Assets/Scripts/Server.cs
new file mode 100644
index 0000000..ef8e37f
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Scripts/Server.cs
@@ -0,0 +1,113 @@
+#nullable enable
+using System;
+using FishNet.Managing;
+using FishNet.Transporting;
+using Newtonsoft.Json.Linq;
+using Rivet;
+using UnityEngine;
+
+public class Server : MonoBehaviour
+{
+
+ #region References
+
+ private RivetAuthenticator _authenticator = null!;
+ private NetworkManager _networkManager = null!;
+ private RivetManager _rivetManager = null!;
+
+ #endregion
+
+ ///
+ /// The game mode to start the game with.
+ ///
+ /// Only available on the server.
+ ///
+ [HideInInspector] public string? gameModeName = null;
+
+ ///
+ /// The lobby config provided for a custom lobby.
+ ///
+ private string? _lobbyConfigRaw = null;
+
+ // Parse LobbyConfigRaw to JObject
+ public JObject? LobbyConfig => _lobbyConfigRaw != null ? JObject.Parse(_lobbyConfigRaw) : null;
+
+
+ // Start is called before the first frame update
+ void Start()
+ {
+ _networkManager = FindObjectOfType();
+ _rivetManager = FindObjectOfType();
+
+ // Configure client authentication
+ _networkManager.ClientManager.OnClientConnectionState += ClientManager_OnClientConnectionState;
+ _networkManager.ClientManager.RegisterBroadcast(OnTokenResponseBroadcast);
+
+ // Start server if testing in editor or running from CLI
+ if (Application.isBatchMode)
+ {
+ StartServer();
+ }
+ }
+
+ #region Server
+
+ private ushort GetServerPort()
+ {
+ var port = Environment.GetEnvironmentVariable("PORT_default");
+ return port != null ? ushort.Parse(port) : (ushort)7770;
+ }
+
+ private void StartServer()
+ {
+ Debug.Log("Starting server on port " + GetServerPort());
+
+ // Read environment variables
+ gameModeName = Environment.GetEnvironmentVariable("RIVET_GAME_MODE_NAME");
+ _lobbyConfigRaw = Environment.GetEnvironmentVariable("RIVET_LOBBY_CONFIG");
+
+ // Start server
+ _networkManager.TransportManager.Transport.SetServerBindAddress("0.0.0.0", IPAddressType.IPv4);
+ _networkManager.TransportManager.Transport.SetPort(GetServerPort());
+ _networkManager.ServerManager.StartConnection();
+ _networkManager.ServerManager.OnRemoteConnectionState += (conn, args) =>
+ {
+ Debug.Log("Remote connection state: " + conn.ClientId + " " + conn.GetAddress() + " " + args.ConnectionState);
+ };
+
+ // Create authentication
+ _authenticator = gameObject.AddComponent();
+ _networkManager.ServerManager.SetAuthenticator(_authenticator);
+
+ // Notify Rivet this server can start accepting players
+ StartCoroutine(_rivetManager.LobbyReady(() => { Debug.Log("Lobby ready"); }, _ => { }));
+ }
+ #endregion
+
+ #region Authentication
+
+ private void ClientManager_OnClientConnectionState(ClientConnectionStateArgs args)
+ {
+ if (args.ConnectionState != LocalConnectionState.Started)
+ return;
+
+ // Send request
+ var token = _rivetManager.FindLobbyResponse?.Player.Token;
+ Debug.Log("Sending authenticate token request: " + token);
+ var pb = new RivetAuthenticator.TokenRequestBroadcast()
+ {
+ Token = token
+ };
+ _networkManager.ClientManager.Broadcast(pb);
+ }
+
+ private void OnTokenResponseBroadcast(RivetAuthenticator.TokenResponseBroadcast trb)
+ {
+ Debug.Log("Token response: " + trb.Valid);
+ string result = (trb.Valid) ? "Token authenticated." : "Token authentication failed.";
+ _networkManager.Log(result);
+ }
+
+ #endregion
+
+}
diff --git a/unity/tanks-fishnet/Assets/Scripts/Server.cs.meta b/unity/tanks-fishnet/Assets/Scripts/Server.cs.meta
new file mode 100644
index 0000000..4382b9f
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/Scripts/Server.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 72c03012efa14404096f106a5024f251
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/StreamingAssets.meta b/unity/tanks-fishnet/Assets/StreamingAssets.meta
new file mode 100644
index 0000000..0e34381
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/StreamingAssets.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a4230ee43ae364ccfb51a20a1fd0eac0
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Assets/rivet_export.asset b/unity/tanks-fishnet/Assets/rivet_export.asset
new file mode 100644
index 0000000..2d9f221
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/rivet_export.asset
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:77c12b8992ce7ca6a2b0ac1d9b2df786d50991bd022da0e36a1223e5abed8f5f
+size 640
diff --git a/unity/tanks-fishnet/Assets/rivet_export.asset.meta b/unity/tanks-fishnet/Assets/rivet_export.asset.meta
new file mode 100644
index 0000000..30f7fa3
--- /dev/null
+++ b/unity/tanks-fishnet/Assets/rivet_export.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47f97534c6e20475185074e379260925
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/tanks-fishnet/Dockerfile b/unity/tanks-fishnet/Dockerfile
deleted file mode 100644
index cadd628..0000000
--- a/unity/tanks-fishnet/Dockerfile
+++ /dev/null
@@ -1,14 +0,0 @@
-FROM debian:12
-
-RUN apt update -y && \
- apt install -y ca-certificates && \
- update-ca-certificates
-
-RUN useradd -m server
-USER server
-
-COPY --chown=server:server ./build/LinuxServer /build
-RUN ls /build && chmod +x /build/LinuxServer.x86_64
-
-EXPOSE 7777/udp
-ENTRYPOINT ["/build/LinuxServer.x86_64", "-batchmode", "-nographics"]
diff --git a/unity/tanks-fishnet/Packages/manifest.json b/unity/tanks-fishnet/Packages/manifest.json
index 494f8f8..430c3e6 100644
--- a/unity/tanks-fishnet/Packages/manifest.json
+++ b/unity/tanks-fishnet/Packages/manifest.json
@@ -5,6 +5,7 @@
"com.unity.nuget.newtonsoft-json": "3.2.1",
"com.unity.textmeshpro": "3.0.6",
"com.unity.timeline": "1.7.6",
+ "com.unity.toolchain.macos-arm64-linux-x86_64": "2.0.0",
"com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.6",
"com.unity.ugui": "1.0.0",
"com.unity.visualscripting": "1.9.1",
diff --git a/unity/tanks-fishnet/Packages/packages-lock.json b/unity/tanks-fishnet/Packages/packages-lock.json
index ab288d7..0615d25 100644
--- a/unity/tanks-fishnet/Packages/packages-lock.json
+++ b/unity/tanks-fishnet/Packages/packages-lock.json
@@ -139,6 +139,16 @@
},
"url": "https://packages.unity.com"
},
+ "com.unity.toolchain.macos-arm64-linux-x86_64": {
+ "version": "2.0.0",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.sysroot": "2.0.7",
+ "com.unity.sysroot.linux-x86_64": "2.0.6"
+ },
+ "url": "https://packages.unity.com"
+ },
"com.unity.toolchain.win-x86_64-linux-x86_64": {
"version": "2.0.6",
"depth": 0,
diff --git a/unity/tanks-fishnet/ProjectSettings/ProjectSettings.asset b/unity/tanks-fishnet/ProjectSettings/ProjectSettings.asset
index 52efcc9..837357e 100644
--- a/unity/tanks-fishnet/ProjectSettings/ProjectSettings.asset
+++ b/unity/tanks-fishnet/ProjectSettings/ProjectSettings.asset
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9b48ab78fbdbbd38f18ba4dc58a30ac0da160b2e0a863cec367b48f3ece5ad69
-size 21730
+oid sha256:84fd2b18885cac8a2029866dfa0802f8cbc67faded1df064460e562f3804fcbf
+size 21941
diff --git a/unity/tanks-fishnet/ProjectSettings/ProjectVersion.txt b/unity/tanks-fishnet/ProjectSettings/ProjectVersion.txt
index 21588a1..307ccd3 100644
--- a/unity/tanks-fishnet/ProjectSettings/ProjectVersion.txt
+++ b/unity/tanks-fishnet/ProjectSettings/ProjectVersion.txt
@@ -1,2 +1,2 @@
-m_EditorVersion: 2022.3.18f1
-m_EditorVersionWithRevision: 2022.3.18f1 (d29bea25151d)
+m_EditorVersion: 2022.3.19f1
+m_EditorVersionWithRevision: 2022.3.19f1 (244b723c30a6)
diff --git a/unity/tanks-fishnet/rivet.yaml b/unity/tanks-fishnet/rivet.yaml
index a194542..c8f2396 100644
--- a/unity/tanks-fishnet/rivet.yaml
+++ b/unity/tanks-fishnet/rivet.yaml
@@ -29,7 +29,8 @@ matchmaker:
enabled: true
docker:
- dockerfile: Dockerfile
+ dockerfile: Assets/Rivet/Dockerfile
+ # Specify that the build
ports:
default:
port: 7770
diff --git a/unity/tanks-fishnet_clone_0/.clone b/unity/tanks-fishnet_clone_0/.clone
new file mode 100644
index 0000000..e69de29
diff --git a/unity/tanks-fishnet_clone_0/.parrelsyncarg b/unity/tanks-fishnet_clone_0/.parrelsyncarg
new file mode 100644
index 0000000..f7629b7
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/.parrelsyncarg
@@ -0,0 +1 @@
+client
\ No newline at end of file
diff --git a/unity/tanks-fishnet_clone_0/.vscode/extensions.json b/unity/tanks-fishnet_clone_0/.vscode/extensions.json
new file mode 100644
index 0000000..ddb6ff8
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/.vscode/extensions.json
@@ -0,0 +1,5 @@
+{
+ "recommendations": [
+ "visualstudiotoolsforunity.vstuc"
+ ]
+}
diff --git a/unity/tanks-fishnet_clone_0/.vscode/launch.json b/unity/tanks-fishnet_clone_0/.vscode/launch.json
new file mode 100644
index 0000000..da60e25
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/.vscode/launch.json
@@ -0,0 +1,10 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Attach to Unity",
+ "type": "vstuc",
+ "request": "attach"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet_clone_0/.vscode/settings.json b/unity/tanks-fishnet_clone_0/.vscode/settings.json
new file mode 100644
index 0000000..be9334c
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/.vscode/settings.json
@@ -0,0 +1,55 @@
+{
+ "files.exclude": {
+ "**/.DS_Store": true,
+ "**/.git": true,
+ "**/.gitmodules": true,
+ "**/*.booproj": true,
+ "**/*.pidb": true,
+ "**/*.suo": true,
+ "**/*.user": true,
+ "**/*.userprefs": true,
+ "**/*.unityproj": true,
+ "**/*.dll": true,
+ "**/*.exe": true,
+ "**/*.pdf": true,
+ "**/*.mid": true,
+ "**/*.midi": true,
+ "**/*.wav": true,
+ "**/*.gif": true,
+ "**/*.ico": true,
+ "**/*.jpg": true,
+ "**/*.jpeg": true,
+ "**/*.png": true,
+ "**/*.psd": true,
+ "**/*.tga": true,
+ "**/*.tif": true,
+ "**/*.tiff": true,
+ "**/*.3ds": true,
+ "**/*.3DS": true,
+ "**/*.fbx": true,
+ "**/*.FBX": true,
+ "**/*.lxo": true,
+ "**/*.LXO": true,
+ "**/*.ma": true,
+ "**/*.MA": true,
+ "**/*.obj": true,
+ "**/*.OBJ": true,
+ "**/*.asset": true,
+ "**/*.cubemap": true,
+ "**/*.flare": true,
+ "**/*.mat": true,
+ "**/*.meta": true,
+ "**/*.prefab": true,
+ "**/*.unity": true,
+ "build/": true,
+ "Build/": true,
+ "Library/": true,
+ "library/": true,
+ "obj/": true,
+ "Obj/": true,
+ "ProjectSettings/": true,
+ "temp/": true,
+ "Temp/": true
+ },
+ "dotnet.defaultSolution": "tanks-fishnet_clone_0.sln"
+}
\ No newline at end of file
diff --git a/unity/tanks-fishnet_clone_0/Assets b/unity/tanks-fishnet_clone_0/Assets
new file mode 120000
index 0000000..bbdc66b
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/Assets
@@ -0,0 +1 @@
+/Users/forest/Documents/git/examples/unity/tanks-fishnet/Assets
\ No newline at end of file
diff --git a/unity/tanks-fishnet_clone_0/Packages/manifest.json b/unity/tanks-fishnet_clone_0/Packages/manifest.json
new file mode 100644
index 0000000..430c3e6
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/Packages/manifest.json
@@ -0,0 +1,45 @@
+{
+ "dependencies": {
+ "com.unity.collab-proxy": "2.2.0",
+ "com.unity.feature.development": "1.0.1",
+ "com.unity.nuget.newtonsoft-json": "3.2.1",
+ "com.unity.textmeshpro": "3.0.6",
+ "com.unity.timeline": "1.7.6",
+ "com.unity.toolchain.macos-arm64-linux-x86_64": "2.0.0",
+ "com.unity.toolchain.win-x86_64-linux-x86_64": "2.0.6",
+ "com.unity.ugui": "1.0.0",
+ "com.unity.visualscripting": "1.9.1",
+ "com.veriorpies.parrelsync": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync",
+ "com.unity.modules.ai": "1.0.0",
+ "com.unity.modules.androidjni": "1.0.0",
+ "com.unity.modules.animation": "1.0.0",
+ "com.unity.modules.assetbundle": "1.0.0",
+ "com.unity.modules.audio": "1.0.0",
+ "com.unity.modules.cloth": "1.0.0",
+ "com.unity.modules.director": "1.0.0",
+ "com.unity.modules.imageconversion": "1.0.0",
+ "com.unity.modules.imgui": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0",
+ "com.unity.modules.particlesystem": "1.0.0",
+ "com.unity.modules.physics": "1.0.0",
+ "com.unity.modules.physics2d": "1.0.0",
+ "com.unity.modules.screencapture": "1.0.0",
+ "com.unity.modules.terrain": "1.0.0",
+ "com.unity.modules.terrainphysics": "1.0.0",
+ "com.unity.modules.tilemap": "1.0.0",
+ "com.unity.modules.ui": "1.0.0",
+ "com.unity.modules.uielements": "1.0.0",
+ "com.unity.modules.umbra": "1.0.0",
+ "com.unity.modules.unityanalytics": "1.0.0",
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.modules.unitywebrequestassetbundle": "1.0.0",
+ "com.unity.modules.unitywebrequestaudio": "1.0.0",
+ "com.unity.modules.unitywebrequesttexture": "1.0.0",
+ "com.unity.modules.unitywebrequestwww": "1.0.0",
+ "com.unity.modules.vehicles": "1.0.0",
+ "com.unity.modules.video": "1.0.0",
+ "com.unity.modules.vr": "1.0.0",
+ "com.unity.modules.wind": "1.0.0",
+ "com.unity.modules.xr": "1.0.0"
+ }
+}
diff --git a/unity/tanks-fishnet_clone_0/Packages/packages-lock.json b/unity/tanks-fishnet_clone_0/Packages/packages-lock.json
new file mode 100644
index 0000000..0615d25
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/Packages/packages-lock.json
@@ -0,0 +1,432 @@
+{
+ "dependencies": {
+ "com.unity.collab-proxy": {
+ "version": "2.2.0",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.editorcoroutines": {
+ "version": "1.0.0",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.ext.nunit": {
+ "version": "1.0.6",
+ "depth": 2,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.feature.development": {
+ "version": "1.0.1",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.ide.visualstudio": "2.0.22",
+ "com.unity.ide.rider": "3.0.27",
+ "com.unity.ide.vscode": "1.2.5",
+ "com.unity.editorcoroutines": "1.0.0",
+ "com.unity.performance.profile-analyzer": "1.2.2",
+ "com.unity.test-framework": "1.1.33",
+ "com.unity.testtools.codecoverage": "1.2.5"
+ }
+ },
+ "com.unity.ide.rider": {
+ "version": "3.0.27",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.ext.nunit": "1.0.6"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.ide.visualstudio": {
+ "version": "2.0.22",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.test-framework": "1.1.9"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.ide.vscode": {
+ "version": "1.2.5",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.nuget.newtonsoft-json": {
+ "version": "3.2.1",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.performance.profile-analyzer": {
+ "version": "1.2.2",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.settings-manager": {
+ "version": "2.0.1",
+ "depth": 2,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.sysroot": {
+ "version": "2.0.7",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {},
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.sysroot.linux-x86_64": {
+ "version": "2.0.6",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.sysroot": "2.0.7"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.test-framework": {
+ "version": "1.1.33",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.ext.nunit": "1.0.6",
+ "com.unity.modules.imgui": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.testtools.codecoverage": {
+ "version": "1.2.5",
+ "depth": 1,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.test-framework": "1.0.16",
+ "com.unity.settings-manager": "1.0.1"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.textmeshpro": {
+ "version": "3.0.6",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.ugui": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.timeline": {
+ "version": "1.7.6",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.modules.director": "1.0.0",
+ "com.unity.modules.animation": "1.0.0",
+ "com.unity.modules.audio": "1.0.0",
+ "com.unity.modules.particlesystem": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.toolchain.macos-arm64-linux-x86_64": {
+ "version": "2.0.0",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.sysroot": "2.0.7",
+ "com.unity.sysroot.linux-x86_64": "2.0.6"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.toolchain.win-x86_64-linux-x86_64": {
+ "version": "2.0.6",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.sysroot": "2.0.7",
+ "com.unity.sysroot.linux-x86_64": "2.0.6"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.unity.ugui": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.ui": "1.0.0",
+ "com.unity.modules.imgui": "1.0.0"
+ }
+ },
+ "com.unity.visualscripting": {
+ "version": "1.9.1",
+ "depth": 0,
+ "source": "registry",
+ "dependencies": {
+ "com.unity.ugui": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0"
+ },
+ "url": "https://packages.unity.com"
+ },
+ "com.veriorpies.parrelsync": {
+ "version": "https://github.com/VeriorPies/ParrelSync.git?path=/ParrelSync",
+ "depth": 0,
+ "source": "git",
+ "dependencies": {},
+ "hash": "141b4e1e31966242a6fb7c495b52ac3d1221bc75"
+ },
+ "com.unity.modules.ai": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.androidjni": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.animation": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.assetbundle": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.audio": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.cloth": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.physics": "1.0.0"
+ }
+ },
+ "com.unity.modules.director": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.audio": "1.0.0",
+ "com.unity.modules.animation": "1.0.0"
+ }
+ },
+ "com.unity.modules.imageconversion": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.imgui": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.jsonserialize": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.particlesystem": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.physics": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.physics2d": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.screencapture": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.imageconversion": "1.0.0"
+ }
+ },
+ "com.unity.modules.subsystems": {
+ "version": "1.0.0",
+ "depth": 1,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.jsonserialize": "1.0.0"
+ }
+ },
+ "com.unity.modules.terrain": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.terrainphysics": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.physics": "1.0.0",
+ "com.unity.modules.terrain": "1.0.0"
+ }
+ },
+ "com.unity.modules.tilemap": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.physics2d": "1.0.0"
+ }
+ },
+ "com.unity.modules.ui": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.uielements": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.ui": "1.0.0",
+ "com.unity.modules.imgui": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0"
+ }
+ },
+ "com.unity.modules.umbra": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.unityanalytics": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0"
+ }
+ },
+ "com.unity.modules.unitywebrequest": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.unitywebrequestassetbundle": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.assetbundle": "1.0.0",
+ "com.unity.modules.unitywebrequest": "1.0.0"
+ }
+ },
+ "com.unity.modules.unitywebrequestaudio": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.modules.audio": "1.0.0"
+ }
+ },
+ "com.unity.modules.unitywebrequesttexture": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.modules.imageconversion": "1.0.0"
+ }
+ },
+ "com.unity.modules.unitywebrequestwww": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.unitywebrequest": "1.0.0",
+ "com.unity.modules.unitywebrequestassetbundle": "1.0.0",
+ "com.unity.modules.unitywebrequestaudio": "1.0.0",
+ "com.unity.modules.audio": "1.0.0",
+ "com.unity.modules.assetbundle": "1.0.0",
+ "com.unity.modules.imageconversion": "1.0.0"
+ }
+ },
+ "com.unity.modules.vehicles": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.physics": "1.0.0"
+ }
+ },
+ "com.unity.modules.video": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.audio": "1.0.0",
+ "com.unity.modules.ui": "1.0.0",
+ "com.unity.modules.unitywebrequest": "1.0.0"
+ }
+ },
+ "com.unity.modules.vr": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.jsonserialize": "1.0.0",
+ "com.unity.modules.physics": "1.0.0",
+ "com.unity.modules.xr": "1.0.0"
+ }
+ },
+ "com.unity.modules.wind": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {}
+ },
+ "com.unity.modules.xr": {
+ "version": "1.0.0",
+ "depth": 0,
+ "source": "builtin",
+ "dependencies": {
+ "com.unity.modules.physics": "1.0.0",
+ "com.unity.modules.jsonserialize": "1.0.0",
+ "com.unity.modules.subsystems": "1.0.0"
+ }
+ }
+ }
+}
diff --git a/unity/tanks-fishnet_clone_0/ProjectSettings b/unity/tanks-fishnet_clone_0/ProjectSettings
new file mode 120000
index 0000000..9fdd261
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/ProjectSettings
@@ -0,0 +1 @@
+/Users/forest/Documents/git/examples/unity/tanks-fishnet/ProjectSettings
\ No newline at end of file
diff --git a/unity/tanks-fishnet_clone_0/collabignore.txt b/unity/tanks-fishnet_clone_0/collabignore.txt
new file mode 100644
index 0000000..f59ec20
--- /dev/null
+++ b/unity/tanks-fishnet_clone_0/collabignore.txt
@@ -0,0 +1 @@
+*
\ No newline at end of file