517 lines
22 KiB
C#
Executable File
517 lines
22 KiB
C#
Executable File
using ksBroadcastingNetwork.Structs;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace ksBroadcastingNetwork
|
|
{
|
|
public enum OutboundMessageTypes : byte
|
|
{
|
|
REGISTER_COMMAND_APPLICATION = 1,
|
|
UNREGISTER_COMMAND_APPLICATION = 9,
|
|
|
|
REQUEST_ENTRY_LIST = 10,
|
|
REQUEST_TRACK_DATA = 11,
|
|
|
|
CHANGE_HUD_PAGE = 49,
|
|
CHANGE_FOCUS = 50,
|
|
INSTANT_REPLAY_REQUEST = 51,
|
|
|
|
PLAY_MANUAL_REPLAY_HIGHLIGHT = 52, // TODO, but planned
|
|
SAVE_MANUAL_REPLAY_HIGHLIGHT = 60 // TODO, but planned: saving manual replays gives distributed clients the possibility to see the play the same replay
|
|
}
|
|
|
|
public enum InboundMessageTypes : byte
|
|
{
|
|
REGISTRATION_RESULT = 1,
|
|
REALTIME_UPDATE = 2,
|
|
REALTIME_CAR_UPDATE = 3,
|
|
ENTRY_LIST = 4,
|
|
ENTRY_LIST_CAR = 6,
|
|
TRACK_DATA = 5,
|
|
BROADCASTING_EVENT = 7
|
|
}
|
|
|
|
public class BroadcastingNetworkProtocol
|
|
{
|
|
public const int BROADCASTING_PROTOCOL_VERSION = 4;
|
|
private string ConnectionIdentifier { get; }
|
|
private SendMessageDelegate Send { get; }
|
|
public int ConnectionId { get; private set; }
|
|
public float TrackMeters { get; private set; }
|
|
|
|
internal delegate void SendMessageDelegate(byte[] payload);
|
|
|
|
#region Events
|
|
|
|
public delegate void ConnectionStateChangedDelegate(int connectionId, bool connectionSuccess, bool isReadonly, string error);
|
|
public event ConnectionStateChangedDelegate OnConnectionStateChanged;
|
|
|
|
public delegate void TrackDataUpdateDelegate(string sender, TrackData trackUpdate);
|
|
public event TrackDataUpdateDelegate OnTrackDataUpdate;
|
|
|
|
public delegate void EntryListUpdateDelegate(string sender, CarInfo car);
|
|
public event EntryListUpdateDelegate OnEntrylistUpdate;
|
|
|
|
public delegate void RealtimeUpdateDelegate(string sender, RealtimeUpdate update);
|
|
public event RealtimeUpdateDelegate OnRealtimeUpdate;
|
|
|
|
public delegate void RealtimeCarUpdateDelegate(string sender, RealtimeCarUpdate carUpdate);
|
|
public event RealtimeCarUpdateDelegate OnRealtimeCarUpdate;
|
|
|
|
public delegate void BroadcastingEventDelegate(string sender, BroadcastingEvent evt);
|
|
public event BroadcastingEventDelegate OnBroadcastingEvent;
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
#region EntryList handling
|
|
|
|
// To avoid huge UDP pakets for longer entry lists, we will first receive the indexes of cars and drivers,
|
|
// cache the entries and wait for the detailled updates
|
|
List<CarInfo> _entryListCars = new List<CarInfo>();
|
|
|
|
#endregion
|
|
|
|
#region optional failsafety - detect when we have a desync and need a new entry list
|
|
|
|
DateTime lastEntrylistRequest = DateTime.Now;
|
|
|
|
#endregion
|
|
|
|
internal BroadcastingNetworkProtocol(string connectionIdentifier, SendMessageDelegate sendMessageDelegate)
|
|
{
|
|
if (string.IsNullOrEmpty(connectionIdentifier))
|
|
throw new ArgumentNullException(nameof(connectionIdentifier), $"No connection identifier set; we use this to distinguish different connections. Using the remote IP:Port is a good idea");
|
|
|
|
if (sendMessageDelegate == null)
|
|
throw new ArgumentNullException(nameof(sendMessageDelegate), $"The protocol class doesn't know anything about the network layer; please put a callback we can use to send data via UDP");
|
|
|
|
ConnectionIdentifier = connectionIdentifier;
|
|
Send = sendMessageDelegate;
|
|
}
|
|
|
|
internal void ProcessMessage(BinaryReader br)
|
|
{
|
|
// Any message starts with an 1-byte command type
|
|
var messageType = (InboundMessageTypes)br.ReadByte();
|
|
switch (messageType)
|
|
{
|
|
case InboundMessageTypes.REGISTRATION_RESULT:
|
|
{
|
|
ConnectionId = br.ReadInt32();
|
|
var connectionSuccess = br.ReadByte() > 0;
|
|
var isReadonly = br.ReadByte() == 0;
|
|
var errMsg = ReadString(br);
|
|
|
|
OnConnectionStateChanged?.Invoke(ConnectionId, connectionSuccess, isReadonly, errMsg);
|
|
|
|
// In case this was successful, we will request the initial data
|
|
RequestEntryList();
|
|
RequestTrackData();
|
|
}
|
|
break;
|
|
case InboundMessageTypes.ENTRY_LIST:
|
|
{
|
|
_entryListCars.Clear();
|
|
|
|
var connectionId = br.ReadInt32();
|
|
var carEntryCount = br.ReadUInt16();
|
|
for (int i = 0; i < carEntryCount; i++)
|
|
{
|
|
_entryListCars.Add(new CarInfo(br.ReadUInt16()));
|
|
}
|
|
}
|
|
break;
|
|
case InboundMessageTypes.ENTRY_LIST_CAR:
|
|
{
|
|
|
|
var carId = br.ReadUInt16();
|
|
|
|
var carInfo = _entryListCars.SingleOrDefault(x => x.CarIndex == carId);
|
|
if(carInfo == null)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"Entry list update for unknown carIndex {carId}");
|
|
break;
|
|
}
|
|
|
|
carInfo.CarModelType = br.ReadByte(); // Byte sized car model
|
|
carInfo.TeamName = ReadString(br);
|
|
carInfo.RaceNumber = br.ReadInt32();
|
|
carInfo.CupCategory = br.ReadByte(); // Cup: Overall/Pro = 0, ProAm = 1, Am = 2, Silver = 3, National = 4
|
|
carInfo.CurrentDriverIndex = br.ReadByte();
|
|
carInfo.Nationality = (NationalityEnum)br.ReadUInt16();
|
|
|
|
// Now the drivers on this car:
|
|
var driversOnCarCount = br.ReadByte();
|
|
for (int di = 0; di < driversOnCarCount; di++)
|
|
{
|
|
var driverInfo = new DriverInfo();
|
|
|
|
driverInfo.FirstName = ReadString(br);
|
|
driverInfo.LastName = ReadString(br);
|
|
driverInfo.ShortName = ReadString(br);
|
|
driverInfo.Category = (DriverCategory)br.ReadByte(); // Platinum = 3, Gold = 2, Silver = 1, Bronze = 0
|
|
|
|
// new in 1.13.11:
|
|
driverInfo.Nationality = (NationalityEnum)br.ReadUInt16();
|
|
|
|
carInfo.AddDriver(driverInfo);
|
|
}
|
|
|
|
OnEntrylistUpdate?.Invoke(ConnectionIdentifier, carInfo);
|
|
}
|
|
break;
|
|
case InboundMessageTypes.REALTIME_UPDATE:
|
|
{
|
|
RealtimeUpdate update = new RealtimeUpdate();
|
|
update.EventIndex = (int)br.ReadUInt16();
|
|
update.SessionIndex = (int)br.ReadUInt16();
|
|
update.SessionType = (RaceSessionType)br.ReadByte();
|
|
update.Phase = (SessionPhase)br.ReadByte();
|
|
var sessionTime = br.ReadSingle();
|
|
update.SessionTime = TimeSpan.FromMilliseconds(sessionTime);
|
|
var sessionEndTime = br.ReadSingle();
|
|
update.SessionEndTime = TimeSpan.FromMilliseconds(sessionEndTime);
|
|
|
|
update.FocusedCarIndex = br.ReadInt32();
|
|
update.ActiveCameraSet = ReadString(br);
|
|
update.ActiveCamera = ReadString(br);
|
|
update.CurrentHudPage = ReadString(br);
|
|
|
|
update.IsReplayPlaying = br.ReadByte() > 0;
|
|
if (update.IsReplayPlaying)
|
|
{
|
|
update.ReplaySessionTime = br.ReadSingle();
|
|
update.ReplayRemainingTime = br.ReadSingle();
|
|
}
|
|
|
|
update.TimeOfDay = TimeSpan.FromMilliseconds(br.ReadSingle());
|
|
update.AmbientTemp = br.ReadByte();
|
|
update.TrackTemp = br.ReadByte();
|
|
update.Clouds = br.ReadByte() / 10.0f;
|
|
update.RainLevel = br.ReadByte() / 10.0f;
|
|
update.Wetness = br.ReadByte() / 10.0f;
|
|
|
|
update.BestSessionLap = ReadLap(br);
|
|
|
|
OnRealtimeUpdate?.Invoke(ConnectionIdentifier, update);
|
|
}
|
|
break;
|
|
case InboundMessageTypes.REALTIME_CAR_UPDATE:
|
|
{
|
|
RealtimeCarUpdate carUpdate = new RealtimeCarUpdate();
|
|
|
|
carUpdate.CarIndex = br.ReadUInt16();
|
|
carUpdate.DriverIndex = br.ReadUInt16(); // Driver swap will make this change
|
|
carUpdate.DriverCount = br.ReadByte();
|
|
carUpdate.Gear = br.ReadByte() - 2; // -2 makes the R -1, N 0 and the rest as-is
|
|
carUpdate.WorldPosX = br.ReadSingle();
|
|
carUpdate.WorldPosY = br.ReadSingle();
|
|
carUpdate.Yaw = br.ReadSingle();
|
|
carUpdate.CarLocation = (CarLocationEnum)br.ReadByte(); // - , Track, Pitlane, PitEntry, PitExit = 4
|
|
carUpdate.Kmh = br.ReadUInt16();
|
|
carUpdate.Position = br.ReadUInt16(); // official P/Q/R position (1 based)
|
|
carUpdate.CupPosition = br.ReadUInt16(); // official P/Q/R position (1 based)
|
|
carUpdate.TrackPosition = br.ReadUInt16(); // position on track (1 based)
|
|
carUpdate.SplinePosition = br.ReadSingle(); // track position between 0.0 and 1.0
|
|
carUpdate.Laps = br.ReadUInt16();
|
|
|
|
carUpdate.Delta = br.ReadInt32(); // Realtime delta to best session lap
|
|
carUpdate.BestSessionLap = ReadLap(br);
|
|
carUpdate.LastLap = ReadLap(br);
|
|
carUpdate.CurrentLap = ReadLap(br);
|
|
|
|
// the concept is: "don't know a car or driver? ask for an entry list update"
|
|
var carEntry = _entryListCars.FirstOrDefault(x => x.CarIndex == carUpdate.CarIndex);
|
|
if(carEntry == null || carEntry.Drivers.Count != carUpdate.DriverCount)
|
|
{
|
|
if ((DateTime.Now - lastEntrylistRequest).TotalSeconds > 1)
|
|
{
|
|
lastEntrylistRequest = DateTime.Now;
|
|
RequestEntryList();
|
|
System.Diagnostics.Debug.WriteLine($"CarUpdate {carUpdate.CarIndex}|{carUpdate.DriverIndex} not know, will ask for new EntryList");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OnRealtimeCarUpdate?.Invoke(ConnectionIdentifier, carUpdate);
|
|
}
|
|
}
|
|
break;
|
|
case InboundMessageTypes.TRACK_DATA:
|
|
{
|
|
var connectionId = br.ReadInt32();
|
|
var trackData = new TrackData();
|
|
|
|
trackData.TrackName = ReadString(br);
|
|
trackData.TrackId = br.ReadInt32();
|
|
trackData.TrackMeters = br.ReadInt32();
|
|
TrackMeters = trackData.TrackMeters > 0 ? trackData.TrackMeters : -1;
|
|
|
|
trackData.CameraSets = new Dictionary<string, List<string>>();
|
|
|
|
var cameraSetCount = br.ReadByte();
|
|
for (int camSet = 0; camSet < cameraSetCount; camSet++)
|
|
{
|
|
var camSetName = ReadString(br);
|
|
trackData.CameraSets.Add(camSetName, new List<string>());
|
|
|
|
var cameraCount = br.ReadByte();
|
|
for (int cam = 0; cam < cameraCount; cam++)
|
|
{
|
|
var cameraName = ReadString(br);
|
|
trackData.CameraSets[camSetName].Add(cameraName);
|
|
}
|
|
}
|
|
|
|
var hudPages = new List<string>();
|
|
var hudPagesCount = br.ReadByte();
|
|
for (int i = 0; i < hudPagesCount; i++)
|
|
{
|
|
hudPages.Add(ReadString(br));
|
|
}
|
|
trackData.HUDPages = hudPages;
|
|
|
|
OnTrackDataUpdate?.Invoke(ConnectionIdentifier, trackData);
|
|
}
|
|
break;
|
|
case InboundMessageTypes.BROADCASTING_EVENT:
|
|
{
|
|
BroadcastingEvent evt = new BroadcastingEvent()
|
|
{
|
|
Type = (BroadcastingCarEventType)br.ReadByte(),
|
|
Msg = ReadString(br),
|
|
TimeMs = br.ReadInt32(),
|
|
CarId = br.ReadInt32(),
|
|
};
|
|
|
|
evt.CarData = _entryListCars.FirstOrDefault(x => x.CarIndex == evt.CarId);
|
|
OnBroadcastingEvent?.Invoke(ConnectionIdentifier, evt);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Laps are always sent in a common way, it makes sense to have a shared function to parse them
|
|
/// </summary>
|
|
private static LapInfo ReadLap(BinaryReader br)
|
|
{
|
|
var lap = new LapInfo();
|
|
lap.LaptimeMS = br.ReadInt32();
|
|
|
|
lap.CarIndex = br.ReadUInt16();
|
|
lap.DriverIndex = br.ReadUInt16();
|
|
|
|
var splitCount = br.ReadByte();
|
|
for (int i = 0; i < splitCount; i++)
|
|
lap.Splits.Add(br.ReadInt32());
|
|
|
|
lap.IsInvalid = br.ReadByte() > 0;
|
|
lap.IsValidForBest = br.ReadByte() > 0;
|
|
|
|
var isOutlap = br.ReadByte() > 0;
|
|
var isInlap = br.ReadByte() > 0;
|
|
|
|
if (isOutlap)
|
|
lap.Type = LapType.Outlap;
|
|
else if (isInlap)
|
|
lap.Type = LapType.Inlap;
|
|
else
|
|
lap.Type = LapType.Regular;
|
|
|
|
// Now it's possible that this is "no" lap that doesn't even include a
|
|
// first split, we can detect this by comparing with int32.Max
|
|
while (lap.Splits.Count < 3)
|
|
{
|
|
lap.Splits.Add(null);
|
|
}
|
|
|
|
// "null" entries are Int32.Max, in the C# world we can replace this to null
|
|
for (int i = 0; i < lap.Splits.Count; i++)
|
|
if (lap.Splits[i] == Int32.MaxValue)
|
|
lap.Splits[i] = null;
|
|
|
|
if (lap.LaptimeMS == Int32.MaxValue)
|
|
lap.LaptimeMS = null;
|
|
|
|
return lap;
|
|
}
|
|
|
|
private static string ReadString(BinaryReader br)
|
|
{
|
|
var length = br.ReadUInt16();
|
|
var bytes = br.ReadBytes(length);
|
|
return Encoding.UTF8.GetString(bytes);
|
|
}
|
|
|
|
private static void WriteString(BinaryWriter bw, string s)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(s);
|
|
bw.Write(Convert.ToUInt16(bytes.Length));
|
|
bw.Write(bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Will try to register this client in the targeted ACC instance.
|
|
/// Needs to be called once, before anything else can happen.
|
|
/// </summary>
|
|
/// <param name="connectionPassword"></param>
|
|
/// <param name="msRealtimeUpdateInterval"></param>
|
|
/// <param name="commandPassword"></param>
|
|
internal void RequestConnection(string displayName, string connectionPassword, int msRealtimeUpdateInterval, string commandPassword)
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var br = new BinaryWriter(ms))
|
|
{
|
|
br.Write((byte)OutboundMessageTypes.REGISTER_COMMAND_APPLICATION); // First byte is always the command type
|
|
br.Write((byte)BROADCASTING_PROTOCOL_VERSION);
|
|
|
|
WriteString(br, displayName);
|
|
WriteString(br, connectionPassword);
|
|
br.Write(msRealtimeUpdateInterval);
|
|
WriteString(br, commandPassword);
|
|
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
|
|
internal void Disconnect()
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var br = new BinaryWriter(ms))
|
|
{
|
|
br.Write((byte)OutboundMessageTypes.UNREGISTER_COMMAND_APPLICATION); // First byte is always the command type
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Will ask the ACC client for an updated entry list, containing all car and driver data.
|
|
/// The client will send this automatically when something changes; however if you detect a carIndex or driverIndex, this may cure the
|
|
/// problem for future updates
|
|
/// </summary>
|
|
private void RequestEntryList()
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var br = new BinaryWriter(ms))
|
|
{
|
|
br.Write((byte)OutboundMessageTypes.REQUEST_ENTRY_LIST); // First byte is always the command type
|
|
br.Write((int)ConnectionId);
|
|
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
|
|
private void RequestTrackData()
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var br = new BinaryWriter(ms))
|
|
{
|
|
br.Write((byte)OutboundMessageTypes.REQUEST_TRACK_DATA); // First byte is always the command type
|
|
br.Write((int)ConnectionId);
|
|
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
|
|
public void SetFocus(UInt16 carIndex)
|
|
{
|
|
SetFocusInternal(carIndex, null, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Always put both cam + cam set; even if it doesn't make sense
|
|
/// </summary>
|
|
public void SetCamera(string cameraSet, string camera)
|
|
{
|
|
SetFocusInternal(null, cameraSet, camera);
|
|
}
|
|
|
|
public void SetFocus(UInt16 carIndex, string cameraSet, string camera)
|
|
{
|
|
SetFocusInternal(carIndex, cameraSet, camera);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends the request to change the focused car and/or the camera used.
|
|
/// The idea is that this often wants to be triggered together, so this is a all-in-one function.
|
|
/// This way we can make sure the switch happens in the same frame, even in more complex scenarios
|
|
/// </summary>
|
|
private void SetFocusInternal(UInt16? carIndex, string cameraSet, string camera)
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var bw = new BinaryWriter(ms))
|
|
{
|
|
bw.Write((byte)OutboundMessageTypes.CHANGE_FOCUS); // First byte is always the command type
|
|
bw.Write((int)ConnectionId);
|
|
|
|
if (!carIndex.HasValue)
|
|
{
|
|
bw.Write((byte)0); // No change of focused car
|
|
}
|
|
else
|
|
{
|
|
bw.Write((byte)1);
|
|
bw.Write((UInt16)(carIndex.Value));
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(cameraSet) || string.IsNullOrEmpty(camera))
|
|
{
|
|
bw.Write((byte)0); // No change of camera set or camera
|
|
}
|
|
else
|
|
{
|
|
bw.Write((byte)1);
|
|
WriteString(bw, cameraSet);
|
|
WriteString(bw, camera);
|
|
}
|
|
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
|
|
public void RequestInstantReplay(float startSessionTime, float durationMS, int initialFocusedCarIndex = -1, string initialCameraSet = "", string initialCamera = "")
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var bw = new BinaryWriter(ms))
|
|
{
|
|
bw.Write((byte)OutboundMessageTypes.INSTANT_REPLAY_REQUEST); // First byte is always the command type
|
|
bw.Write((int)ConnectionId);
|
|
|
|
bw.Write((float)startSessionTime);
|
|
bw.Write((float)durationMS);
|
|
bw.Write((int)initialFocusedCarIndex);
|
|
|
|
WriteString(bw, initialCameraSet);
|
|
WriteString(bw, initialCamera);
|
|
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
|
|
public void RequestHUDPage(string hudPage)
|
|
{
|
|
using (var ms = new MemoryStream())
|
|
using (var bw = new BinaryWriter(ms))
|
|
{
|
|
bw.Write((byte)OutboundMessageTypes.CHANGE_HUD_PAGE); // First byte is always the command type
|
|
bw.Write((int)ConnectionId);
|
|
|
|
WriteString(bw, hudPage);
|
|
|
|
Send(ms.ToArray());
|
|
}
|
|
}
|
|
}
|
|
}
|