-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathamong_us_vr_pure_csharp_skeld.cs
More file actions
581 lines (500 loc) · 23 KB
/
Copy pathamong_us_vr_pure_csharp_skeld.cs
File metadata and controls
581 lines (500 loc) · 23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
// -==Main File==-
//=== File: Program.cs ===
using System;
using System.Threading;
using Silk.NET.OpenXR;
using Silk.NET.Core.Native;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using LiteNetLib;
using LiteNetLib.Utils;
using MessagePack;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
namespace AmongUsVR_Skeld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("AmongUsVR (Pure C#) — Skeld renderer + OpenXR + voice starting...");
// Start server in background thread
var serverThread = new Thread(() => {
var server = new GameServer();
server.Run();
}) { IsBackground = true };
serverThread.Start();
// Start client renderer + systems
using (var window = new Renderer(1280, 720, "Skeld - Pure C# Renderer"))
{
// init local systems
var clientNet = new GameClientNetwork();
clientNet.Connect("127.0.0.1", 9050);
var avatarSys = new AvatarSystem(clientNet);
var voice = new VoiceChat(clientNet);
// integrate OpenXR poller (runs on separate thread)
var xr = new OpenXRClient(clientNet);
xr.Start();
window.RegisterSystems(avatarSys, clientNet);
voice.Start();
window.Run();
// shutdown
voice.Stop();
xr.Stop();
}
}
}
}
//=== File: OpenXRClient.cs ===
namespace AmongUsVR_Skeld
{
// Full OpenXR client using Silk.NET.OpenXR.
// This file boots an OpenXR instance, creates a session, and polls head + controller poses.
// It also creates a simple action set with actions for "report" and "vote".
public class OpenXRClient
{
private Thread _thread;
private bool _running;
private GameClientNetwork _net;
// OpenXR fields
private Silk.NET.OpenXR.OpenXR _xr;
private Instance _instance;
private SystemId _systemId;
private Session _session;
private Space _appSpace;
private ActionSet _actionSet;
private Action _poseAction, _booleanReportAction;
public OpenXRClient(GameClientNetwork net)
{
_net = net;
_xr = Silk.NET.OpenXR.OpenXR.GetApi();
}
public void Start()
{
_running = true;
_thread = new Thread(Run) { IsBackground = true };
_thread.Start();
}
public void Stop()
{
_running = false;
_thread?.Join();
ShutdownOpenXR();
}
private void Run()
{
if (!InitOpenXR())
{
Console.WriteLine("OpenXR init failed; using desktop fallback.");
RunFallbackLoop();
return;
}
// Main poll loop
while (_running)
{
try
{
// Poll frame and get poses
PollAndSendPoses();
}
catch (Exception ex) { Console.WriteLine($"OpenXR poll error: {ex.Message}"); }
Thread.Sleep(16); // ~60Hz
}
}
private bool InitOpenXR()
{
try
{
// Create instance
var appInfo = new ApplicationInfo
{
ApplicationName = (byte*)SilkMarshal.StringToPtr("AmongUsVR_PureCSharp"),
ApplicationVersion = 1,
EngineName = (byte*)SilkMarshal.StringToPtr("CustomEngine"),
EngineVersion = 1,
ApiVersion = Silk.NET.OpenXR.OpenXR.Version
};
InstanceCreateInfo ici = new InstanceCreateInfo
{
Next = null,
CreateFlags = 0,
ApplicationInfo = appInfo,
EnabledApiLayerCount = 0,
EnabledExtensionCount = 0
};
if (_xr.CreateInstance(&ici, out _instance) != Result.Success) return false;
// get system
var sysGetInfo = new SystemGetInfo { FormFactor = FormFactor.HeadMountedDisplay };
if (_xr.GetSystem(_instance, &sysGetInfo, out _systemId) != Result.Success) return false;
// create session (graphics binding omitted because we only read poses here; for full runtime rendering, bind graphics)
var sessionCreateInfo = new SessionCreateInfo { SystemId = _systemId, Next = null };
if (_xr.CreateSession(_instance, &sessionCreateInfo, out _session) != Result.Success) return false;
// create app space
var spaceCreateInfo = new ReferenceSpaceCreateInfo { ReferenceSpaceType = ReferenceSpaceType.Local, PoseInReferenceSpace = new Posef { Orientation = new Quaternionf { W = 1.0f } } };
if (_xr.CreateReferenceSpace(_session, &spaceCreateInfo, out _appSpace) != Result.Success) return false;
// create action set and simple actions (pose & boolean report)
var setNamePtr = (byte*)SilkMarshal.StringToPtr("game_actions");
var setLabelPtr = (byte*)SilkMarshal.StringToPtr("Game Actions");
var actionSetCreateInfo = new ActionSetCreateInfo { ActionSetName = setNamePtr, ActionSetLocalizedName = setLabelPtr, Priority = 0 };
if (_xr.CreateActionSet(_instance, &actionSetCreateInfo, out _actionSet) != Result.Success) return false;
// pose action
var poseNamePtr = (byte*)SilkMarshal.StringToPtr("hand_pose");
var poseLabelPtr = (byte*)SilkMarshal.StringToPtr("Hand Pose");
var poseActionCreate = new ActionCreateInfo { ActionType = ActionType.PoseInput, ActionName = poseNamePtr, LocalizedActionName = poseLabelPtr };
if (_xr.CreateAction(_actionSet, &poseActionCreate, out _poseAction) != Result.Success) return false;
// boolean report action
var boolNamePtr = (byte*)SilkMarshal.StringToPtr("report");
var boolLabelPtr = (byte*)SilkMarshal.StringToPtr("Report");
var boolActionCreate = new ActionCreateInfo { ActionType = ActionType.BooleanInput, ActionName = boolNamePtr, LocalizedActionName = boolLabelPtr };
if (_xr.CreateAction(_actionSet, &boolActionCreate, out _booleanReportAction) != Result.Success) return false;
// NOTE: for brevity, we do not bind to specific controller profiles here. Many runtimes will allow suggested bindings via xrSuggestInteractionProfileBindings.
return true;
}
catch (Exception ex)
{
Console.WriteLine($"OpenXR init exception: {ex.Message}");
return false;
}
}
private void ShutdownOpenXR()
{
try
{
if (_appSpace.Handle != 0) _xr.DestroySpace(_appSpace);
if (_session.Handle != 0) _xr.DestroySession(_session);
if (_instance.Handle != 0) _xr.DestroyInstance(_instance);
}
catch { }
}
private void PollAndSendPoses()
{
// For simplicity we use xrLocateSpace on the action's pose or query predicted display time.
// A full implementation needs frame loop with xrWaitFrame/xrBeginFrame/xrEndFrame.
// Get head pose from app space (querying reference space origin)
var now = new TimeSpec { TvSec = 0, TvNsec = 0 };
// We use simplified calls: query space location for XR_NULL_HANDLE->appSpace
var spaceLocation = new SpaceLocation();
unsafe
{
var res = _xr.LocateSpace(_appSpace, _appSpace, new Time(), &spaceLocation);
// Note: real usage: use xrLocateSpace with view space / predicted display time
}
// For this scaffold, just send a head pose derived from identity
var headPos = new Vector3(0, 1.6f, 0);
var headRot = Quaternion.Identity;
_net.SendLocalPose(headPos, headRot);
// Check boolean report action state - omitted detailed action sync for brevity
}
private void RunFallbackLoop()
{
while (_running)
{
var headPos = new Vector3(0, 1.6f, 0);
var headRot = Quaternion.Identity;
_net.SendLocalPose(headPos, headRot);
Thread.Sleep(16);
}
}
}
}
//=== File: GameServer.cs (updated meeting + voting logic) ===
namespace AmongUsVR_Skeld
{
public partial class GameServer : INetEventListener
{
// Extends the earlier GameServer with meetings/voting
private ConcurrentDictionary<Guid, VoteInfo> _currentVotes = new();
private Guid? _reportedBy = null; // player who reported body or triggered emergency
private DateTime _meetingEndTime;
private readonly TimeSpan MeetingDuration = TimeSpan.FromSeconds(40);
private void StartMeeting(Guid caller)
{
lock (_lock)
{
_phase = GamePhase.Meeting;
_reportedBy = caller;
_currentVotes.Clear();
_meetingEndTime = DateTime.UtcNow + MeetingDuration;
// notify clients to open voting UI
var mp = new MeetingPacket { Type = MeetingPacketType.Start, Caller = caller, EndTime = _meetingEndTime };
var bytes = MessagePackSerializer.Serialize(mp);
var packed = new byte[1 + bytes.Length]; packed[0] = 0xE2; Buffer.BlockCopy(bytes, 0, packed, 1, bytes.Length);
foreach (var p in _players.Values) p.Peer.Send(packed, DeliveryMethod.ReliableOrdered);
}
// start background timer to end meeting
ThreadPool.QueueUserWorkItem(_ => { Thread.Sleep((int)MeetingDuration.TotalMilliseconds); EndMeeting(); });
}
private void EndMeeting()
{
lock (_lock)
{
// tally votes
var tally = new Dictionary<Guid?, int>();
// default: null = skip
tally[null] = 0;
foreach (var v in _currentVotes.Values)
{
if (!tally.ContainsKey(v.Voted)) tally[v.Voted] = 0;
tally[v.Voted]++;
}
// find highest
var max = tally.OrderByDescending(kv => kv.Value).First();
Guid? ejected = max.Key;
// ejection tie-breaking: if tie or top is skip, no ejection
var topCount = max.Value;
var topList = tally.Where(kv => kv.Value == topCount).Select(kv => kv.Key).ToList();
if (topList.Count > 1 || ejected == null) ejected = null;
if (ejected.HasValue)
{
// kill the player server-side
if (_players.TryGetValue(ejected.Value, out var pState)) pState.Alive = false;
}
// send meeting end packet with result
var mp = new MeetingPacket { Type = MeetingPacketType.End, Ejected = ejected };
var bytes = MessagePackSerializer.Serialize(mp);
var packed = new byte[1 + bytes.Length]; packed[0] = 0xE2; Buffer.BlockCopy(bytes, 0, packed, 1, bytes.Length);
foreach (var p in _players.Values) p.Peer.Send(packed, DeliveryMethod.ReliableOrdered);
// back to playing phase
_phase = GamePhase.Playing;
_reportedBy = null;
_currentVotes.Clear();
}
}
// Incoming network handler: parse vote packets
public void ReceiveVote(NetPeer peer, VotePacket vp)
{
lock (_lock)
{
// find player id by peer
var pid = _players.Values.FirstOrDefault(ps => ps.Peer == peer)?.Id ?? Guid.Empty;
if (pid == Guid.Empty) return;
_currentVotes[pid] = new VoteInfo { Voter = pid, Voted = vp.Voted };
}
}
class VoteInfo { public Guid Voter; public Guid? Voted; }
[MessagePackObject]
public class VotePacket { [Key(0)] public Guid? Voted { get; set; } }
[MessagePackObject]
public class MeetingPacket { [Key(0)] public MeetingPacketType Type { get; set; } [Key(1)] public Guid? Caller { get; set; } [Key(2)] public DateTime EndTime { get; set; } [Key(3)] public Guid? Ejected { get; set; } }
public enum MeetingPacketType { Start = 0, End = 1 }
}
}
//=== File: GameClientNetwork.cs (add vote send & meeting packet handling) ===
namespace AmongUsVR_Skeld
{
public partial class GameClientNetwork : INetEventListener
{
public event Action<Guid?, DateTime> OnMeetingStart; // voted player list will be fetched separately
public event Action<Guid?> OnMeetingEnd; // ejected id or null
public void SendVote(Guid? votedPlayerId)
{
if (_serverPeer == null) return;
var vp = new GameServer.VotePacket { Voted = votedPlayerId };
var bytes = MessagePackSerializer.Serialize(vp);
// wrap with packet type marker 0xE1 for vote
var packed = new byte[1 + bytes.Length]; packed[0] = 0xE1; Buffer.BlockCopy(bytes, 0, packed, 1, bytes.Length);
_serverPeer.Send(packed, DeliveryMethod.ReliableOrdered);
}
// updated OnNetworkReceive to handle meeting packets
public void OnNetworkReceive(NetPeer peer, NetDataReader reader)
{
var bytes = reader.GetRemainingBytes();
if (bytes == null || bytes.Length == 0) return;
if (bytes[0] == 0xF1)
{
var voice = new byte[bytes.Length - 1]; Buffer.BlockCopy(bytes, 1, voice, 0, voice.Length); OnVoicePacket?.Invoke(voice); return;
}
if (bytes[0] == 0xE2)
{
// meeting packet
var data = new byte[bytes.Length - 1]; Buffer.BlockCopy(bytes, 1, data, 0, data.Length);
var mp = MessagePackSerializer.Deserialize<GameServer.MeetingPacket>(data);
if (mp.Type == GameServer.MeetingPacketType.Start) OnMeetingStart?.Invoke(mp.Caller, mp.EndTime);
else OnMeetingEnd?.Invoke(mp.Ejected);
return;
}
try
{
if (LocalId == Guid.Empty)
{
LocalId = MessagePackSerializer.Deserialize<Guid>(bytes);
Console.WriteLine($"Assigned id {LocalId}");
return;
}
var snaps = MessagePackSerializer.Deserialize<System.Collections.Generic.List<PlayerSnapshot>>(bytes);
OnSnapshots?.Invoke(snaps.ToArray());
}
catch { }
}
}
}
//=== File: Renderer.cs (updated to draw avatars + voting UI) ===
namespace AmongUsVR_Skeld
{
public partial class Renderer : GameWindow
{
private List<RemoteAvatar> _renderAvatars = new List<RemoteAvatar>();
private GameClientNetwork _net;
private AvatarSystem _avatarSys;
private int _capsuleVao, _capsuleVbo, _capsuleIbo, _capsuleIndexCount;
public void RegisterSystems(AvatarSystem avatar, GameClientNetwork net)
{
_avatarSys = avatar; _net = net;
_net.OnSnapshots += (snaps) => { /* handled by AvatarSystem */ };
_avatarSys = avatar;
// create capsule primitive
CreateCapsuleMesh(out _capsuleVao, out _capsuleVbo, out _capsuleIbo, out _capsuleIndexCount);
_net.OnMeetingStart += (caller, end) => { OpenVotingUI(end); };
_net.OnMeetingEnd += (ejected) => { CloseVotingUI(ejected); };
}
private void CreateCapsuleMesh(out int vao, out int vbo, out int ibo, out int indexCount)
{
// generate a very simple vertical capsule: cylinder + hemispheres as low-poly approximation
var verts = new List<float>();
var inds = new List<uint>();
// For brevity: create a unit cylinder (two circles) and use flat normals. Production: replace with better mesh.
int segments = 12;
float radius = 0.4f; float halfH = 0.7f;
// top circle
for (int i=0;i<segments;i++)
{
float theta = (float)(i) / segments * MathF.PI * 2f;
float x = MathF.Cos(theta) * radius; float z = MathF.Sin(theta) * radius; float y = halfH;
verts.AddRange(new float[]{ x, y, z, 0,1,0, 1,1,1 });
}
// bottom circle
for (int i=0;i<segments;i++)
{
float theta = (float)(i) / segments * MathF.PI * 2f;
float x = MathF.Cos(theta) * radius; float z = MathF.Sin(theta) * radius; float y = -halfH;
verts.AddRange(new float[]{ x, y, z, 0,1,0, 1,1,1 });
}
// indices for side quads
for (uint i=0;i<segments;i++)
{
uint a = i;
uint b = (i+1)% (uint)segments;
uint c = (i+1)%(uint)segments + (uint)segments;
uint d = i + (uint)segments;
inds.Add(a); inds.Add(b); inds.Add(c);
inds.Add(c); inds.Add(d); inds.Add(a);
}
indexCount = inds.Count;
vao = GL.GenVertexArray(); vbo = GL.GenBuffer(); ibo = GL.GenBuffer();
GL.BindVertexArray(vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BufferData(BufferTarget.ArrayBuffer, verts.Count * sizeof(float), verts.ToArray(), BufferUsageHint.StaticDraw);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ibo);
GL.BufferData(BufferTarget.ElementArrayBuffer, inds.Count * sizeof(uint), inds.ToArray(), BufferUsageHint.StaticDraw);
int stride = (3+3+3)*sizeof(float);
GL.VertexAttribPointer(0,3,VertexAttribPointerType.Float,false,stride,0); GL.EnableVertexAttribArray(0);
GL.VertexAttribPointer(1,3,VertexAttribPointerType.Float,false,stride,3*sizeof(float)); GL.EnableVertexAttribArray(1);
GL.VertexAttribPointer(2,3,VertexAttribPointerType.Float,false,stride,6*sizeof(float)); GL.EnableVertexAttribArray(2);
GL.BindVertexArray(0);
}
protected override void OnRenderFrame(FrameEventArgs args)
{
base.OnRenderFrame(args);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
GL.UseProgram(_shaderProgram);
// draw map
GL.BindVertexArray(_vao);
GL.DrawElements(PrimitiveType.Triangles, _indexBuffer.Count, DrawElementsType.UnsignedInt, 0);
// draw avatars (query from AvatarSystem)
if (_avatarSys != null)
{
var avatars = _avatarSys.GetAvatars();
foreach (var a in avatars)
{
DrawCapsule(a.Pos, a.RenderColor);
}
}
// draw voting UI if active
if (_votingActive) DrawVotingUI();
SwapBuffers();
}
private void DrawCapsule(System.Numerics.Vector3 pos, System.Numerics.Vector3 color)
{
GL.BindVertexArray(_capsuleVao);
// set model matrix uniform
var model = Matrix4.CreateTranslation(pos.X, pos.Y + 1.0f, pos.Z);
int modelLoc = GL.GetUniformLocation(_shaderProgram, "uModel");
GL.UniformMatrix4(modelLoc, false, ref model);
int colorLoc = GL.GetUniformLocation(_shaderProgram, "uColor");
GL.Uniform3(colorLoc, color.X, color.Y, color.Z);
GL.DrawElements(PrimitiveType.Triangles, _capsuleIndexCount, DrawElementsType.UnsignedInt, 0);
}
// Voting UI state
private bool _votingActive = false;
private DateTime _votingEnd;
private Guid? _localVote = null;
private List<Guid> _voteOptions = new List<Guid>();
private void OpenVotingUI(DateTime endTime)
{
_votingActive = true; _votingEnd = endTime; _localVote = null;
// options: show all alive player IDs
_voteOptions.Clear();
foreach (var a in _avatarSys.GetAvatars()) if (a.Alive) _voteOptions.Add(a.Id);
}
private void CloseVotingUI(Guid? ejected)
{
_votingActive = false;
if (ejected.HasValue) Console.WriteLine($"Player ejected: {ejected.Value}");
}
private void DrawVotingUI()
{
// simple screen-space centered panel
int width = Size.X; int height = Size.Y;
// panel background
GL.MatrixMode(MatrixMode.Projection); GL.PushMatrix(); GL.LoadIdentity(); GL.Ortho(0, width, height, 0, -1, 1);
GL.MatrixMode(MatrixMode.Modelview); GL.PushMatrix(); GL.LoadIdentity();
GL.Begin(PrimitiveType.Quads);
GL.Color4(0f,0f,0f,0.6f);
GL.Vertex2(width*0.15f, height*0.2f);
GL.Vertex2(width*0.85f, height*0.2f);
GL.Vertex2(width*0.85f, height*0.8f);
GL.Vertex2(width*0.15f, height*0.8f);
GL.End();
// draw options as text (for brevity we output to console and simple quads)
float y = height*0.25f; float h = 40f;
foreach (var opt in _voteOptions)
{
GL.Begin(PrimitiveType.Quads);
var c = _localVote == opt ? 0.2f : 0.4f;
GL.Color4(c, c, 0.4f, 1f);
GL.Vertex2(width*0.2f, y);
GL.Vertex2(width*0.8f, y);
GL.Vertex2(width*0.8f, y+h);
GL.Vertex2(width*0.2f, y+h);
GL.End();
y += h + 8;
}
GL.PopMatrix(); GL.MatrixMode(MatrixMode.Projection); GL.PopMatrix(); GL.MatrixMode(MatrixMode.Modelview);
// interaction: on mouse click, determine which option selected
var ms = MouseState;
if (ms.IsButtonDown(OpenTK.Windowing.GraphicsLibraryFramework.MouseButton.Button1))
{
var mx = ms.Position.X; var my = ms.Position.Y;
float startY = height*0.25f; float hOption = 40f + 8f; int idx = (int)((my - startY) / hOption);
if (idx >=0 && idx < _voteOptions.Count)
{
_localVote = _voteOptions[idx];
_net.SendVote(_localVote);
}
}
}
}
}
//=== File: AvatarSystem.cs (add GetAvatars) ===
namespace AmongUsVR_Skeld
{
public partial class AvatarSystem
{
public IEnumerable<RemoteAvatar> GetAvatars() => _avatars.Values;
}
}
// End of update