Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ tracked in each package's `package.json`.

## Unreleased

- Expanded `com.zerogamestudio.zeroengine.multiplayer` with Multipass route selection, pre-transport game preparation, post-connect local synchronization, automatic client reconnect/restore, authenticated remote-start confirmation, and reconnect-focused coordinator tests.
- Added root project documentation.
- Added contribution, support, and security guidance.
- Added MIT licensing.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mixing in game-specific content.
| `com.zerogamestudio.zeroengine.data` | Data and stat systems for game configuration and runtime values. |
| `com.zerogamestudio.zeroengine.data-toolkit` | Editor tooling for browsing, inspecting, and validating project data assets. |
| `com.zerogamestudio.zeroengine.gameplay` | Reusable gameplay mechanics and trigger helpers. |
| `com.zerogamestudio.zeroengine.multiplayer` | Platform-neutral room, session, invite, retry, and reconnect orchestration. |
| `com.zerogamestudio.zeroengine.narrative` | Quest and narrative runtime services. |
| `com.zerogamestudio.zeroengine.pathfinding2d` | 2D platform navigation, graph generation, jump links, route costs, and diagnostics. |
| `com.zerogamestudio.zeroengine.persistence` | Save and persistence infrastructure. |
Expand Down
16 changes: 16 additions & 0 deletions com.zerogamestudio.zeroengine.multiplayer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

## [0.2.0] - Unreleased

- Add LocalDirect room descriptors, command-line parsing, and connection-driven member snapshots.
- Add optional FishNet 4.6.15 connection driver, authenticated identity bridge, and fail-closed remote admission gate for Tugboat and FishySteamworks.
- Add optional Steamworks.NET 2024.8.0 runtime ownership, lobby metadata, room lifecycle, invitation, host-loss handling, cancellation-safe native operations, and live Lobby membership authorization.
- Publish host phase, joinability, and monotonic session-generation changes through platform-neutral room-state contracts.
- Add setup validation, configuration inspector, local two-process launcher, and LocalDirect Room UI sample.

## [0.1.0]

- Add platform-neutral room, connection, and game-adapter contracts.
- Add configurable session, compatibility, invite, reconnect, and stable-seat rules.
- Add a serialized multiplayer session coordinator and read-only presentation state.
- Add fake-backed EditMode coverage for the M1 core package.
7 changes: 7 additions & 0 deletions com.zerogamestudio.zeroengine.multiplayer/CHANGELOG.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions com.zerogamestudio.zeroengine.multiplayer/Editor.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
using System;
using System.Diagnostics;
using System.IO;
using UnityEditor;
using UnityEngine;
using ZeroEngine.Multiplayer.Local;
using Debug = UnityEngine.Debug;

namespace ZeroEngine.Multiplayer.Editor
{
public sealed class LocalMultiplayerLauncher : EditorWindow
{
private string _playerExecutable = string.Empty;
private string _address = "127.0.0.1";
private int _port = 7770;
private string _roomId = "local-room";
private string _sessionId = "local-session";
private string _productId = "sample";
private string _protocolVersion = "1";
private string _gameProtocolVersion = "1";
private string _contentRevision = "sample";
private string _buildVersion = "development";
private string _gameRoomId = "sample-room";
private bool _headless = true;
private bool _exitOnReady;

[MenuItem("Window/ZeroEngine/Multiplayer/Local Launcher")]
private static void Open()
{
GetWindow<LocalMultiplayerLauncher>("Local Multiplayer");
}

private void OnGUI()
{
EditorGUILayout.LabelField("Built Player", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
_playerExecutable = EditorGUILayout.TextField("Executable", _playerExecutable);
if (GUILayout.Button("Browse", GUILayout.Width(70f)))
{
string selected = EditorUtility.OpenFilePanel("Select built player", string.Empty, "exe");
if (!string.IsNullOrEmpty(selected))
{
_playerExecutable = selected;
}
}
EditorGUILayout.EndHorizontal();

EditorGUILayout.Space();
EditorGUILayout.LabelField("LocalDirect", EditorStyles.boldLabel);
_address = EditorGUILayout.TextField("Address", _address);
_port = EditorGUILayout.IntField("Port", _port);
_roomId = EditorGUILayout.TextField("Room ID", _roomId);
_sessionId = EditorGUILayout.TextField("Session ID", _sessionId);
_headless = EditorGUILayout.Toggle("Headless", _headless);
_exitOnReady = EditorGUILayout.Toggle("Exit On Ready", _exitOnReady);

EditorGUILayout.Space();
EditorGUILayout.LabelField("Compatibility", EditorStyles.boldLabel);
_productId = EditorGUILayout.TextField("Product", _productId);
_protocolVersion = EditorGUILayout.TextField("Protocol", _protocolVersion);
_gameProtocolVersion = EditorGUILayout.TextField("Game Protocol", _gameProtocolVersion);
_contentRevision = EditorGUILayout.TextField("Content", _contentRevision);
_buildVersion = EditorGUILayout.TextField("Build", _buildVersion);
_gameRoomId = EditorGUILayout.TextField("Game Room", _gameRoomId);

EditorGUILayout.Space();
using (new EditorGUI.DisabledScope(!CanLaunch()))
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Launch Host"))
{
Launch(LocalMultiplayerRole.Host);
}

if (GUILayout.Button("Launch Client"))
{
Launch(LocalMultiplayerRole.Client);
}

if (GUILayout.Button("Launch Both"))
{
Launch(LocalMultiplayerRole.Host);
Launch(LocalMultiplayerRole.Client);
}
EditorGUILayout.EndHorizontal();
}

if (GUILayout.Button("Open Logs Folder"))
{
Directory.CreateDirectory(GetLogsDirectory());
EditorUtility.RevealInFinder(GetLogsDirectory());
}
}

public static string BuildProcessArguments(
LocalDevelopmentRoomOptions options,
string logPath,
bool headless)
{
string arguments = LocalMultiplayerLaunchArguments.Build(options) +
" -logFile " + Quote(logPath);
if (headless)
{
arguments += " -batchmode -nographics";
}

return arguments;
}

private bool CanLaunch()
{
return File.Exists(_playerExecutable) && _port > 0 && _port <= ushort.MaxValue &&
!string.IsNullOrWhiteSpace(_roomId) && !string.IsNullOrWhiteSpace(_sessionId);
}

private void Launch(LocalMultiplayerRole role)
{
CompatibilityDescriptor compatibility = new CompatibilityDescriptor(
_productId,
_gameProtocolVersion,
_contentRevision,
_buildVersion,
_gameRoomId);
PlatformUser host = new PlatformUser(new PlatformUserId("local-host"), "Local Host");
PlatformUser client = new PlatformUser(new PlatformUserId("local-client"), "Local Client");
LocalDevelopmentRoomOptions options = new LocalDevelopmentRoomOptions(
role,
_address,
(ushort)_port,
new RoomId(_roomId),
new SessionId(_sessionId),
1,
role == LocalMultiplayerRole.Host ? host : client,
host,
role == LocalMultiplayerRole.Host ? client : host,
compatibility,
_protocolVersion,
2,
RoomVisibility.Private,
_exitOnReady);

OperationResult validation = options.Validate();
if (!validation.Succeeded)
{
Debug.LogError("[ZeroEngine.Multiplayer] Launcher options invalid: " + validation.MessageKey);
return;
}

string logs = GetLogsDirectory();
Directory.CreateDirectory(logs);
string roleName = role == LocalMultiplayerRole.Host ? "host" : "client";
string logPath = Path.Combine(logs, roleName + "-" + DateTime.Now.ToString("yyyyMMdd-HHmmssfff") + ".log");
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = _playerExecutable,
Arguments = BuildProcessArguments(options, logPath, _headless),
WorkingDirectory = Path.GetDirectoryName(_playerExecutable) ?? Environment.CurrentDirectory,
UseShellExecute = false,
CreateNoWindow = _headless
};
Process.Start(startInfo);
Debug.Log("[ZeroEngine.Multiplayer] Launched " + roleName + ". Log: " + logPath);
}

private static string GetLogsDirectory()
{
return Path.GetFullPath(Path.Combine(Application.dataPath, "..", "Logs", "LocalMultiplayer"));
}

private static string Quote(string value)
{
return "\"" + (value ?? string.Empty).Replace("\"", "\\\"") + "\"";
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace ZeroEngine.Multiplayer.Editor
{
[CustomEditor(typeof(MultiplayerSessionConfig))]
public sealed class MultiplayerConfigEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();

MultiplayerSessionConfig config = (MultiplayerSessionConfig)target;
IReadOnlyList<MultiplayerSetupIssue> issues = MultiplayerSetupValidator.ValidateConfig(config);
if (issues.Count == 0)
{
EditorGUILayout.HelpBox("Configuration values are valid.", MessageType.Info);
return;
}

for (int i = 0; i < issues.Count; i++)
{
EditorGUILayout.HelpBox(
issues[i].Code + "\n" + issues[i].Message,
issues[i].Severity == MultiplayerSetupIssueSeverity.Error
? MessageType.Error
: MessageType.Warning);
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading