Skip to content

Commit

Permalink
Merge pull request #21 from geeooff/bugfix/console-resize
Browse files Browse the repository at this point in the history
Console size below minimum size now shows an error and UTF-8 (re)enforced
  • Loading branch information
geeooff authored Mar 20, 2024
2 parents fc8abea + f904ce4 commit b393b27
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 100 deletions.
250 changes: 151 additions & 99 deletions Console/ForzaDataConsole.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class ForzaDataConsole : ForzaDataObserver
╚══════════════════════════════════════════════════╧═════════════════════╝
".Trim();

private static readonly (int width, int height) BlankScreenSize = GetMinimumSize(BlankScreen);

private const ConsoleColor DefaultValueColor = ConsoleColor.White;
private const ConsoleColor DefaultEngineLowRpmColor = ConsoleColor.Green;
private const ConsoleColor DefaultEngineMediumRpmColor = ConsoleColor.Yellow;
Expand All @@ -36,9 +38,12 @@ public class ForzaDataConsole : ForzaDataObserver
private const float PsiToBarDivisor = 14.503773773022f;
private const float StandardGravity = 9.80665f; // m/s²

private ForzaDataStruct? previousData;
private bool hadEnoughSpace;

public ForzaDataConsole()
{

hadEnoughSpace = true;
}

public virtual ConsoleColor ValueColor { get; set; } = DefaultValueColor;
Expand All @@ -51,38 +56,73 @@ public ForzaDataConsole()
public virtual ConsoleColor GearDriveColor { get; set; } = DefaultGearDriveColor;
public virtual ConsoleColor GearReverseColor { get; set; } = DefaultGearReverseColor;

private ForzaDataStruct? previousData;

public override void OnCompleted()
{
System.Console.Clear();
System.Console.Error.WriteLine("Listening completed");
lock (SyncRoot)
{
ClearUI();
System.Console.Error.WriteLine("Listening completed");
}
}

public override void OnError(ForzaDataException error)
{
System.Console.Clear();
System.Console.Error.WriteLine(error);
lock (SyncRoot)
{
ClearUI();
System.Console.Error.WriteLine(error);
}
}

public override void OnError(Exception error)
{
System.Console.Clear();
System.Console.Error.WriteLine(error);
lock (SyncRoot)
{
ClearUI();
System.Console.Error.WriteLine(error);
}
}

public override void OnNext(ForzaDataStruct value)
{
if (ShouldResetUI(value))
InitializeUI();
lock (SyncRoot)
{
bool hasEnoughSpace = HasEnoughSpace();

UpdateUI(value);
if (hasEnoughSpace)
{
if (ShouldResetUI(value) || !hadEnoughSpace)
InitializeUI();

UpdateUI(value);
}
else if (hadEnoughSpace)
{
ShowNotEnoughWindowSpaceMessage();
}

previousData = value;
previousData = value;
hadEnoughSpace = hasEnoughSpace;
}
}

protected virtual void ClearUI()
{
System.Console.Clear();
System.Console.CursorTop = 0;
System.Console.CursorLeft = 0;
System.Console.CursorVisible = false;
}

protected virtual void InitializeUI()
{
ClearUI();
System.Console.Write(BlankScreen);
}

protected virtual bool ShouldResetUI(ForzaDataStruct data)
{
// first time receiving data
if (!previousData.HasValue)
return true;

Expand All @@ -92,96 +132,99 @@ protected virtual bool ShouldResetUI(ForzaDataStruct data)
|| (previousData.Value.Sled.IsRaceOn != data.Sled.IsRaceOn);
}

protected virtual void InitializeUI()
protected virtual bool HasEnoughSpace()
{
lock (SyncRoot)
{
System.Console.Clear();
System.Console.CursorTop = 0;
System.Console.CursorLeft = 0;
System.Console.CursorVisible = false;
System.Console.Write(BlankScreen);
}
(int width, int height) = (
Math.Min(System.Console.BufferWidth, System.Console.WindowWidth),
Math.Min(System.Console.BufferHeight, System.Console.WindowHeight)
);

return width >= BlankScreenSize.width
&& height >= BlankScreenSize.height;
}

protected virtual void ShowNotEnoughWindowSpaceMessage()
{
ClearUI();
System.Console.WriteLine("Not enough space to display the UI.");
System.Console.WriteLine($"Please set your console to at least {BlankScreenSize.width}x{BlankScreenSize.height} chars");
}

protected virtual void UpdateUI(ForzaDataStruct data)
{
ForzaSledDataStruct sd = data.Sled;

lock (SyncRoot)
// race on/off
string raceIsOnValue = ' ' + GetRaceIsOneValue(sd.IsRaceOn) + ' ';
ConsoleWriteAt(3, 0, raceIsOnValue, raceIsOnValue.Length, GetRaceIsOneValueColor(sd.IsRaceOn));

// we don't display any other value if race is not on
if (sd.IsRaceOn == 0)
return;

// header, line 1
ConsoleWriteAt(7, 1, $"{sd.CarOrdinal,7:0}", 7);
ConsoleWriteAt(24, 1, GetCarClassValue(sd.CarClass, sd.CarPerformanceIndex) ?? "XX", 2);
ConsoleWriteAt(35, 1, $"{sd.CarPerformanceIndex,3:0}", 3);
ConsoleWriteAt(53, 1, GetDriveTrainValue(sd.DrivetrainType), 3);
ConsoleWriteAt(70, 1, $"{sd.NumCylinders,2:0}", 2);

// engine, rotation per minute
ConsoleWriteAt(12, 4, $"{sd.CurrentEngineRpm,6:0}", 6, GetCurrentEngineRpmValueColor(sd.CurrentEngineRpm, sd.EngineMaxRpm));
ConsoleWriteAt(12, 5, $"{sd.EngineIdleRpm,6:0}", 6);
ConsoleWriteAt(12, 6, $"{sd.EngineMaxRpm,6:0}", 6);

// acceleration, m/s² -> Gs
ConsoleWriteAt(16, 10, $"{sd.AccelerationX / StandardGravity,7:'◄ '#0.00;'► '#0.00;0.00}", 7);
ConsoleWriteAt(28, 10, $"{sd.AccelerationY / StandardGravity,7:'▼ '#0.00;'▲ '#0.00;0.00}", 7);
ConsoleWriteAt(40, 10, $"{sd.AccelerationZ / StandardGravity,7:'▼ '#0.00;'▲ '#0.00;0.00}", 7);

// velocity, m/s
ConsoleWriteAt(16, 11, $"{sd.VelocityX,6:'◄ '##0.0;'► '##0.0;0.0}", 6);
ConsoleWriteAt(28, 11, $"{sd.VelocityY,6:'▲ '##0.0;'▼ '##0.0;0.0}", 6);
ConsoleWriteAt(40, 11, $"{sd.VelocityZ,6:'▲ '##0.0;'▼ '##0.0;0.0}", 6);

// position
// TODO: need to figure the unit
ConsoleWriteAt(15, 14, $"{sd.Yaw,11:0.000000}", 11);
ConsoleWriteAt(27, 14, $"{sd.Pitch,11:0.000000}", 11);
ConsoleWriteAt(39, 14, $"{sd.Roll,11:0.000000}", 11);

// angular velocity
// TODO: need to figure the unit
ConsoleWriteAt(15, 15, $"{sd.AngularVelocityX,11:0.000000}", 11);
ConsoleWriteAt(27, 15, $"{sd.AngularVelocityY,11:0.000000}", 11);
ConsoleWriteAt(39, 15, $"{sd.AngularVelocityZ,11:0.000000}", 11);

if (data.CarDash.HasValue)
{
// race on/off
string raceIsOnValue = ' ' + GetRaceIsOneValue(sd.IsRaceOn) + ' ';
ConsoleWriteAt(3, 0, raceIsOnValue, raceIsOnValue.Length, GetRaceIsOneValueColor(sd.IsRaceOn));

// we don't display any other value if race is not on
if (sd.IsRaceOn == 0)
return;

// header, line 1
ConsoleWriteAt(7, 1, $"{sd.CarOrdinal,7:0}", 7);
ConsoleWriteAt(24, 1, GetCarClassValue(sd.CarClass, sd.CarPerformanceIndex) ?? "XX", 2);
ConsoleWriteAt(35, 1, $"{sd.CarPerformanceIndex,3:0}", 3);
ConsoleWriteAt(53, 1, GetDriveTrainValue(sd.DrivetrainType), 3);
ConsoleWriteAt(70, 1, $"{sd.NumCylinders,2:0}", 2);

// engine, rotation per minute
ConsoleWriteAt(12, 4, $"{sd.CurrentEngineRpm,6:0}", 6, GetCurrentEngineRpmValueColor(sd.CurrentEngineRpm, sd.EngineMaxRpm));
ConsoleWriteAt(12, 5, $"{sd.EngineIdleRpm,6:0}", 6);
ConsoleWriteAt(12, 6, $"{sd.EngineMaxRpm,6:0}", 6);

// acceleration, m/s² -> Gs
ConsoleWriteAt(16, 10, $"{sd.AccelerationX / StandardGravity,7:'◄ '#0.00;'► '#0.00;0.00}", 7);
ConsoleWriteAt(28, 10, $"{sd.AccelerationY / StandardGravity,7:'▼ '#0.00;'▲ '#0.00;0.00}", 7);
ConsoleWriteAt(40, 10, $"{sd.AccelerationZ / StandardGravity,7:'▼ '#0.00;'▲ '#0.00;0.00}", 7);

// velocity, m/s
ConsoleWriteAt(16, 11, $"{sd.VelocityX,6:'◄ '##0.0;'► '##0.0;0.0}", 6);
ConsoleWriteAt(28, 11, $"{sd.VelocityY,6:'▲ '##0.0;'▼ '##0.0;0.0}", 6);
ConsoleWriteAt(40, 11, $"{sd.VelocityZ,6:'▲ '##0.0;'▼ '##0.0;0.0}", 6);

// position
// TODO: need to figure the unit
ConsoleWriteAt(15, 14, $"{sd.Yaw,11:0.000000}", 11);
ConsoleWriteAt(27, 14, $"{sd.Pitch,11:0.000000}", 11);
ConsoleWriteAt(39, 14, $"{sd.Roll,11:0.000000}", 11);

// angular velocity
// TODO: need to figure the unit
ConsoleWriteAt(15, 15, $"{sd.AngularVelocityX,11:0.000000}", 11);
ConsoleWriteAt(27, 15, $"{sd.AngularVelocityY,11:0.000000}", 11);
ConsoleWriteAt(39, 15, $"{sd.AngularVelocityZ,11:0.000000}", 11);

if (data.CarDash.HasValue)
{
ForzaCarDashDataStruct cdd = data.CarDash.Value;

// header, line 2
ConsoleWriteAt(10, 2, $"{cdd.LapNumber + 1}", 4);
ConsoleWriteAt(28, 2, $"{cdd.RacePosition}", 3);
ConsoleWriteAt(41, 2, $"{cdd.Fuel * 100f,5:0.0}", 5);
ConsoleWriteAt(63, 2, $"{cdd.DistanceTraveled / 1000f,6:0.0}", 6);

// speed (m/s -> km/h), power (watts -> killowatts), torque (newton-meter), boost (psi -> bar)
ConsoleWriteAt(32, 4, $"{cdd.Speed * 3.6f,7:0.0}", 7);
ConsoleWriteAt(32, 5, $"{cdd.Power / 1000f,7:0.0}", 7);
ConsoleWriteAt(32, 6, $"{cdd.Torque,7:0.0}", 7);
ConsoleWriteAt(32, 7, $"{cdd.Boost / PsiToBarDivisor,7:0.00}", 7);

// times
ConsoleWriteAt(59, 4, GetRaceTimeValue(cdd.CurrentLap), 13);
ConsoleWriteAt(59, 5, GetRaceTimeValue(cdd.LastLap), 13);
ConsoleWriteAt(59, 6, GetRaceTimeValue(cdd.BestLap), 13);
ConsoleWriteAt(59, 7, GetRaceTimeValue(cdd.CurrentRaceTime), 13);

// controls, in percentages
ConsoleWriteAt(67, 9, $"{cdd.Accel / 2.55f,3:0}", 3);
ConsoleWriteAt(67, 10, $"{cdd.Brake / 2.55f,3:0}", 3);
ConsoleWriteAt(67, 11, $"{cdd.Clutch / 2.55f,3:0}", 3);
ConsoleWriteAt(67, 12, $"{cdd.HandBrake / 2.55f,3:0}", 3);
ConsoleWriteAt(68, 13, GetGearValue(cdd.Gear), 2, GetGearValueColor(cdd.Gear));
ConsoleWriteAt(65, 14, $"{cdd.Steer / 1.27f,5:'► '##0;'◄ '##0;0}", 5);
}
ForzaCarDashDataStruct cdd = data.CarDash.Value;

// header, line 2
ConsoleWriteAt(10, 2, $"{cdd.LapNumber + 1}", 4);
ConsoleWriteAt(28, 2, $"{cdd.RacePosition}", 3);
ConsoleWriteAt(41, 2, $"{cdd.Fuel * 100f,5:0.0}", 5);
ConsoleWriteAt(63, 2, $"{cdd.DistanceTraveled / 1000f,6:0.0}", 6);

// speed (m/s -> km/h), power (watts -> killowatts), torque (newton-meter), boost (psi -> bar)
ConsoleWriteAt(32, 4, $"{cdd.Speed * 3.6f,7:0.0}", 7);
ConsoleWriteAt(32, 5, $"{cdd.Power / 1000f,7:0.0}", 7);
ConsoleWriteAt(32, 6, $"{cdd.Torque,7:0.0}", 7);
ConsoleWriteAt(32, 7, $"{cdd.Boost / PsiToBarDivisor,7:0.00}", 7);

// times
ConsoleWriteAt(59, 4, GetRaceTimeValue(cdd.CurrentLap), 13);
ConsoleWriteAt(59, 5, GetRaceTimeValue(cdd.LastLap), 13);
ConsoleWriteAt(59, 6, GetRaceTimeValue(cdd.BestLap), 13);
ConsoleWriteAt(59, 7, GetRaceTimeValue(cdd.CurrentRaceTime), 13);

// controls, in percentages
ConsoleWriteAt(67, 9, $"{cdd.Accel / 2.55f,3:0}", 3);
ConsoleWriteAt(67, 10, $"{cdd.Brake / 2.55f,3:0}", 3);
ConsoleWriteAt(67, 11, $"{cdd.Clutch / 2.55f,3:0}", 3);
ConsoleWriteAt(67, 12, $"{cdd.HandBrake / 2.55f,3:0}", 3);
ConsoleWriteAt(68, 13, GetGearValue(cdd.Gear), 2, GetGearValueColor(cdd.Gear));
ConsoleWriteAt(65, 14, $"{cdd.Steer / 1.27f,5:'► '##0;'◄ '##0;0}", 5);
}
}

Expand Down Expand Up @@ -264,16 +307,16 @@ protected void ConsoleWriteAt(int left, int top, char value, ConsoleColor? color

protected void ConsoleWriteAt(int left, int top, string value, int length, ConsoleColor? color = null)
{
int bufferHeight = System.Console.BufferHeight;
int bufferWidth = System.Console.BufferWidth;
int windowHeight = System.Console.WindowHeight;
int windowWidth = System.Console.WindowWidth;

// avoid buffer-overflow writing
if (top > bufferHeight || left > bufferWidth)
if (top > windowHeight || left > windowWidth)
return;

System.Console.SetCursorPosition(left, top);

int availableChars = Math.Min(length, bufferWidth - left);
int availableChars = Math.Min(length, windowWidth - left);
if (value.Length > availableChars)
{
Debug.WriteLine($"Substring occured on value \"{value}\" because of {availableChars} available chars");
Expand Down Expand Up @@ -304,4 +347,13 @@ protected void ConsoleWriteAt(int left, int top, string value, int length, Conso
System.Console.ForegroundColor = prevFgColor.Value;
}
}

private static (int width, int height) GetMinimumSize(string value)
{
var lines = value.Split('\n').Select(line => line.Trim('\r'));
return (
lines.Max(line => line.Length),
lines.Count()
);
}
}
8 changes: 7 additions & 1 deletion Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@ static class Program
DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicNestedTypes,
typeof(DefaultCommand)
)]
static int Main(string[] args) => new CommandApp<DefaultCommand>().Run(args);
static int Main(string[] args)
{
// enforcing UTF-8 output
System.Console.OutputEncoding = System.Text.Encoding.UTF8;

return new CommandApp<DefaultCommand>().Run(args);
}
}

0 comments on commit b393b27

Please sign in to comment.