Skip to content

Commit 6b8c6b5

Browse files
NathanFlurryAngelOnFira
authored andcommitted
chore: add game server & backend panel (#37)
1 parent 2a1cae8 commit 6b8c6b5

19 files changed

Lines changed: 529 additions & 87 deletions

Assets/Rivet/Editor/TaskManager.cs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System.Collections.Generic;
2+
using System.Diagnostics;
3+
using System.Threading.Tasks;
4+
using Codice.Client.BaseCommands;
5+
using Newtonsoft.Json;
6+
using Newtonsoft.Json.Linq;
7+
using Rivet.Editor.UI.TaskPanel;
8+
using UnityEditor;
9+
10+
namespace Rivet.Editor
11+
{
12+
public struct TaskConfig
13+
{
14+
public string Name;
15+
public JObject Input;
16+
}
17+
18+
19+
/// <summary>
20+
/// Persistent task that can be stopped and restarted with logs.
21+
/// </summary>
22+
public class TaskManager
23+
{
24+
const int MAX_LOG_LEN = 10_000;
25+
26+
public enum LogType { META, STDOUT, STDERR }
27+
28+
public struct LogEntry
29+
{
30+
public string Message;
31+
public LogType Type;
32+
33+
public LogEntry(string message, LogType type)
34+
{
35+
Message = message;
36+
Type = type;
37+
}
38+
}
39+
40+
public delegate Task<TaskConfig?> GetTaskConfigDelegate();
41+
public delegate TaskPanelWindow? GetTaskPanelDelegate();
42+
public delegate void StateChangeHandler(bool running);
43+
44+
// Config
45+
private GetTaskConfigDelegate _getTaskConfig;
46+
private GetTaskPanelDelegate _getTaskPanel;
47+
private bool _autoRestart = false;
48+
private bool _taskStopping = false;
49+
50+
// Events
51+
public event StateChangeHandler StateChange;
52+
53+
// State
54+
private readonly object _taskLock = new();
55+
private RivetTask _task;
56+
public List<LogEntry> LogEntries = new();
57+
58+
public TaskManager(string initMessage, GetTaskConfigDelegate getTaskConfig, GetTaskPanelDelegate getTaskPanel, bool autoRestart = false)
59+
{
60+
_getTaskConfig = getTaskConfig;
61+
_getTaskPanel = getTaskPanel;
62+
_autoRestart = autoRestart;
63+
64+
LogEntries.Add(new LogEntry(initMessage, LogType.META));
65+
}
66+
67+
public async Task StartTask(bool restart = true)
68+
{
69+
lock (_taskLock)
70+
{
71+
// Do nothing if already stopping another task
72+
if (_taskStopping)
73+
return;
74+
75+
// Do nothing if task already running
76+
if (!restart && _task != null)
77+
return;
78+
}
79+
80+
// Kill old task
81+
StopTask();
82+
83+
// Start task
84+
var config = await _getTaskConfig();
85+
if (config == null)
86+
{
87+
RivetLogger.Log("No task config provided.");
88+
}
89+
lock (_taskLock)
90+
{
91+
_task = new RivetTask(config.Value.Name, config.Value.Input);
92+
_taskStopping = false;
93+
_task.OnLog += OnTaskLog;
94+
OnStateChange();
95+
}
96+
97+
AddLogLine("Start", LogType.META);
98+
99+
// Run task
100+
var output = await _task.RunAsync();
101+
lock (_taskLock)
102+
{
103+
_task = null;
104+
OnStateChange();
105+
}
106+
107+
// Log output
108+
switch (output)
109+
{
110+
case ResultOk<JObject> ok:
111+
AddLogLine($"Exited with {ok.Data.ToString(Formatting.None)}", LogType.META);
112+
break;
113+
case ResultErr<JObject> err:
114+
AddLogLine($"Task error: {err.Message}", LogType.META);
115+
break;
116+
}
117+
118+
// TODO: Re-enable task restarting
119+
// // Restart if task was not stopped
120+
// bool shouldRestart;
121+
// lock (_taskLock) shouldRestart = _autoRestart && _task == null && !_taskStopping;
122+
// if (shouldRestart)
123+
// {
124+
// AddLogLine("Restarting in 2 seconds", LogType.META);
125+
// await Task.Delay(2000);
126+
// await StartTask();
127+
// }
128+
}
129+
130+
public void StopTask()
131+
{
132+
lock (_taskLock)
133+
{
134+
if (_task != null)
135+
{
136+
_taskStopping = true;
137+
_task.Kill();
138+
_task = null;
139+
140+
AddLogLine("Stop", LogType.META);
141+
142+
OnStateChange();
143+
}
144+
}
145+
}
146+
147+
private void OnTaskLog(string message, RivetTask.LogType type)
148+
{
149+
LogType logType = type switch
150+
{
151+
RivetTask.LogType.STDOUT => LogType.STDOUT,
152+
RivetTask.LogType.STDERR => LogType.STDERR,
153+
_ => LogType.META
154+
};
155+
AddLogLine(message, logType);
156+
}
157+
158+
public void AddLogLine(string message, LogType type)
159+
{
160+
LogEntries.Add(new LogEntry(message, type));
161+
if (LogEntries.Count > MAX_LOG_LEN)
162+
{
163+
LogEntries.RemoveRange(0, LogEntries.Count - MAX_LOG_LEN);
164+
}
165+
166+
// This might not be called on the main thread
167+
EditorApplication.delayCall += () => _getTaskPanel()?.UpdateLogs();
168+
}
169+
170+
public void ClearLogs()
171+
{
172+
LogEntries.Clear();
173+
_getTaskPanel()?.UpdateLogs();
174+
}
175+
176+
private void OnStateChange()
177+
{
178+
// HACK: Ignore _task.IsRunning because we can't hook in to the
179+
// event right after the task is started (where IsRunning is set to
180+
// true). To fix this, we need to be able to hook in to state change
181+
// events on RivetTask.
182+
StateChange.Invoke(_task != null);
183+
}
184+
}
185+
}

Assets/Rivet/Editor/TaskManager.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Assets/Rivet/Editor/UI/RivetPlugin.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Newtonsoft.Json;
77
using System.Collections.Generic;
88
using Rivet.UI.Screens;
9+
using Rivet.Editor.UI.TaskPanel;
910

1011
namespace Rivet.Editor.UI
1112
{
@@ -17,6 +18,8 @@ public enum Screen
1718

1819
public class RivetPlugin : EditorWindow
1920
{
21+
public static RivetPlugin? Singleton;
22+
2023
[SerializeField]
2124
private VisualTreeAsset m_VisualTreeAsset;
2225

@@ -27,18 +30,20 @@ public class RivetPlugin : EditorWindow
2730
private VisualElement _screenLogin;
2831
private VisualElement _screenMain;
2932

30-
private LoginController _loginController;
31-
private MainController _mainController;
33+
public LoginController LoginController;
34+
public MainController MainController;
3235

33-
[MenuItem("Window/UI Toolkit/RivetPlugin")]
34-
public static void ShowExample()
36+
[MenuItem("Window/Rivet/Rivet", false, 20)]
37+
public static void ShowPlugin()
3538
{
36-
RivetPlugin wnd = GetWindow<RivetPlugin>();
37-
wnd.titleContent = new GUIContent("RivetPlugin");
39+
RivetPlugin window = GetWindow<RivetPlugin>();
40+
window.titleContent = new GUIContent("Rivet");
3841
}
3942

4043
public void CreateGUI()
4144
{
45+
Singleton = this;
46+
4247
PluginSettings.LoadSettings();
4348

4449
m_VisualTreeAsset.CloneTree(rootVisualElement);
@@ -54,8 +59,8 @@ public void CreateGUI()
5459
rootVisualElement.Q(className: "discordButton").RegisterCallback<ClickEvent>((ev) => Application.OpenURL("https://rivet.gg/discord"));
5560
rootVisualElement.Q(className: "feedbackButton").RegisterCallback<ClickEvent>((ev) => Application.OpenURL("https://hub.rivet.gg/?modal=feedback&utm=unity"));
5661

57-
_loginController = new LoginController(this, _screenLogin);
58-
_mainController = new MainController(this, _screenMain);
62+
LoginController = new LoginController(this, _screenLogin);
63+
MainController = new MainController(this, _screenMain);
5964

6065
SetScreen(Screen.Login);
6166
}

Assets/Rivet/Editor/UI/Screens/MainController.cs

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using Rivet.Editor;
55
using Rivet.Editor.Types;
66
using Rivet.Editor.UI;
7+
using Rivet.Editor.UI.TaskPanel;
8+
using Rivet.Editor.Util;
79
using Rivet.UI.Tabs;
810
using Unity.VisualScripting.YamlDotNet.Core.Tokens;
911
using UnityEditor;
@@ -28,6 +30,10 @@ public class MainController
2830
private readonly RivetPlugin _pluginWindow;
2931
private readonly VisualElement _root;
3032

33+
// MARK: Tasks
34+
public TaskManager LocalGameServerManager;
35+
public TaskManager BackendManager;
36+
3137
// MARK: Bootstrap
3238
public BootstrapData? BootstrapData;
3339

@@ -83,16 +89,84 @@ public int RemoteEnvironmentIndex
8389
}
8490
}
8591

92+
// MARK: Local Game Server
93+
public string? LocalGameServerExecutablePath;
94+
8695
public MainController(RivetPlugin window, VisualElement root)
8796
{
8897
_pluginWindow = window;
8998
_root = root;
9099

100+
// Task managers
101+
LocalGameServerManager = new(
102+
initMessage: "Open \"Develop\" and press \"Start\" to start game server.",
103+
getTaskConfig: async () =>
104+
{
105+
if (LocalGameServerExecutablePath != null)
106+
{
107+
return new TaskConfig
108+
{
109+
Name = "exec_command",
110+
Input = new JObject
111+
{
112+
["cwd"] = Builder.ProjectRoot(),
113+
["cmd"] = LocalGameServerExecutablePath,
114+
["args"] = new JArray { "-batchmode", "-nographics", "-server" },
115+
}
116+
};
117+
}
118+
else
119+
{
120+
return null;
121+
}
122+
},
123+
getTaskPanel: () => GameServerWindow.GetWindowIfExists()
124+
);
125+
126+
BackendManager = new(
127+
initMessage: "Auto-started by Rivet plugin.",
128+
getTaskConfig: async () =>
129+
{
130+
// Choose port to run on. This is to avoid potential conflicts with
131+
// multiple projects running at the same time.
132+
var chooseRes = await new RivetTask("backend_choose_local_port", new JObject()).RunAsync();
133+
int port;
134+
switch (chooseRes)
135+
{
136+
case ResultOk<JObject> ok:
137+
port = (int)ok.Data["port"];
138+
break;
139+
case ResultErr<JObject> err:
140+
RivetLogger.Error($"Failed to choose port: {err}");
141+
return null;
142+
default:
143+
return null;
144+
}
145+
146+
147+
return new TaskConfig
148+
{
149+
Name = "backend_dev",
150+
Input = new JObject
151+
{
152+
["port"] = port,
153+
["cwd"] = Builder.ProjectRoot(),
154+
}
155+
};
156+
},
157+
getTaskPanel: () => BackendWindow.GetWindowIfExists(),
158+
autoRestart: true
159+
);
160+
161+
// UI
91162
InitUI();
92-
93163
SetTab(MainTab.Setup);
94164

165+
// Fetch data
95166
_ = GetBootstrapData();
167+
168+
// Start backend
169+
_ = BackendManager.StartTask();
96170
}
97171

98172
void InitUI()
@@ -118,7 +192,7 @@ void InitUI()
118192
_settingsTabButton = tabBar.Q(name: "Settings");
119193
_settingsTabBody = tabBody.Q(name: "Settings");
120194
_settingsTabButton.RegisterCallback<ClickEvent>(ev => SetTab(MainTab.Settings));
121-
_settingsController = new SettingsController(_pluginWindow, _settingsTabBody);
195+
_settingsController = new SettingsController(_pluginWindow, this, _settingsTabBody);
122196
}
123197

124198
void SetTab(MainTab tab)
@@ -143,7 +217,7 @@ private async Task GetBootstrapData()
143217
var result = await new RivetTask("get_bootstrap_data", new JObject()).RunAsync();
144218
if (result is ResultErr<JObject>) return;
145219

146-
var data = result.Data.ToObject<BootstrapData>();;
220+
var data = result.Data.ToObject<BootstrapData>(); ;
147221
BootstrapData = data;
148222
ExtensionData.ApiEndpoint = data.ApiEndpoint;
149223

Assets/Rivet/Editor/UI/Tabs/Develop.uxml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</ui:Instance>
1818
<ui:VisualElement name="LocalGameServerBody" style="flex-grow: 0; flex-shrink: 0;">
1919
<ui:VisualElement name="ButtonRow" style="flex-grow: 1; flex-direction: row; margin-bottom: 4px;">
20-
<ui:Instance template="IconButton" name="StartButton" style="flex-grow: 1;">
20+
<ui:Instance template="IconButton" name="StartButton" style="flex-grow: 1; display: flex;">
2121
<AttributeOverrides element-name="Label" text="Start" />
2222
</ui:Instance>
2323
<ui:Instance template="IconButton" name="StopButton" style="flex-grow: 1;">

0 commit comments

Comments
 (0)