Add save and loader classes and some QoL improvements

This commit is contained in:
Stone_Red
2023-07-29 23:38:48 +02:00
parent 61a0166599
commit 6d0ca85b7f
33 changed files with 875 additions and 153 deletions
@@ -1,8 +1,8 @@
<Project StylesheetPath="C:\Users\David\My Drive\Programming\StoneRed.LogicSimulator\StoneRed.LogicSimulator\Content\stylesheet\default_ui_skin.xmms">
<Project.ExportOptions />
<VerticalStackPanel BorderThickness="1" Padding="0, 10, 0, 5" Background="#333130FF" Border="#1BA1E2FF">
<Label Text="rteterert" Margin="10, 0" ClipToBounds="True" Id="title" />
<TextButton Text="Settings" Margin="10, 10, 10, 0" BorderThickness="1" Padding="5" HorizontalAlignment="Center" Scale="0.9, 0.9" Border="#5BC6FAFF" />
<Label Text="Context Menu" Margin="10, 0" ClipToBounds="True" Id="title" />
<TextButton Text="Settings" Margin="10, 10, 10, 0" BorderThickness="1" Padding="5" HorizontalAlignment="Center" Scale="0.9, 0.9" Border="#5BC6FAFF" Id="button" />
<VerticalMenu HorizontalAlignment="Stretch" Margin="0, 10, 0, 0" BorderThickness="0, 1, 0, 0" Padding="0, 10, 0, 5" Background="#333130FF" Id="menu" />
</VerticalStackPanel>
</Project>
@@ -1,9 +1,11 @@
<Project StylesheetPath="stylesheet/default_ui_skin.xmms">
<Project.ExportOptions />
<Window Title="Quick Menu" Left="394" Top="177">
<Window Title="Quick Menu" Left="376" Top="149">
<VerticalMenu HorizontalAlignment="Center" Margin="0, 0, 0, 15" Padding="0, 10" Id="menu">
<MenuItem Text="Main Menu" Id="menuMainMenu" />
<MenuItem Text="Settings" Id="menuSettings" />
<MenuItem Text="Save" Id="menuSave" />
<MenuItem Text="Load" Id="menuLoad" />
<MenuItem Text="Quit" Id="menuQuit" />
</VerticalMenu>
</Window>
@@ -1,6 +1,6 @@
<Project StylesheetPath="stylesheet/default_ui_skin.xmms">
<Project.ExportOptions />
<Grid>
<Grid CanSelectNothing="True" >
<Grid.ColumnsProportions>
<Proportion Type="Part" />
<Proportion Type="Part" Value="3" />
@@ -9,12 +9,19 @@
<VerticalStackPanel Id="info">
<Label Text="FPS:" TextColor="#CF56BFFF" Id="fps" />
<Label Text="FRQ: " TextColor="#CF56BFFF" Id="ups" />
<Label Text="X/Y: " TextColor="#CF56BFFF" Id="position" />
</VerticalStackPanel>
<Panel GridColumn="2" Background="#4BD961FF">
<VerticalStackPanel Id="settings">
<Label Text="Frequency:" />
<SpinButton Maximum="1E+09" Minimum="1" HorizontalAlignment="Stretch" Value="100" Integer="True" Id="frequency" />
<CheckBox Text="High Performance" Id="highPerformance" />
<Label Text="Native components:" Wrap="True" />
<ListBox HorizontalAlignment="Stretch" Id="nativeComponents" />
<Label Text="Custom components:" Wrap="True" />
<ListBox HorizontalAlignment="Stretch">
<ListItem Text="This is a test 2" />
</ListBox>
</VerticalStackPanel>
</Panel>
</Grid>
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Minor Code Smell", "S1643:Strings should not be concatenated using '+' in a loop", Justification = "<Pending>", Scope = "member", Target = "~M:StoneRed.LogicSimulator.Utilities.LogicGatesManager.LoadLogicGates")]
@@ -10,8 +10,10 @@ namespace StoneRed.LogicSimulator.Simulation;
internal class LogicGateSimulator
{
private readonly ConcurrentDictionary<int, LogicGate> logicGates = new ConcurrentDictionary<int, LogicGate>();
private readonly ConcurrentDictionary<ulong, LogicGate> logicGates = new ConcurrentDictionary<ulong, LogicGate>();
private DateTime dateTime;
private bool logicGatesUpdated = false;
private ulong logicGateId = 0;
public int TargetTicksPerSecond { get; set; } = 100;
public int ActualTicksPerSecond { get; private set; }
@@ -22,7 +24,7 @@ internal class LogicGateSimulator
public bool HighPerformanceClock { get; set; }
public LogicGateSimulator(List<LogicGate> logicGates)
public LogicGateSimulator(IEnumerable<LogicGate> logicGates)
{
foreach (LogicGate logicGate in logicGates)
{
@@ -49,11 +51,11 @@ internal class LogicGateSimulator
public void AddLogicGate(LogicGate gate)
{
gate.Id = logicGates.Count;
_ = logicGates.TryAdd(gate.Id, gate);
gate.Id = logicGateId++;
logicGatesUpdated = logicGates.TryAdd(gate.Id, gate);
}
public LogicGate GetLogicGate(int id)
public LogicGate GetLogicGate(ulong id)
{
return logicGates[id];
}
@@ -65,7 +67,12 @@ internal class LogicGateSimulator
public void RemoveLogicGate(LogicGate logicGate)
{
_ = logicGates.TryRemove(logicGate.Id, out _);
foreach (LogicGate otherLogicGate in logicGates.Values.Where(l => l.IsConnectedTo(logicGate)))
{
otherLogicGate.Disconnect(logicGate);
}
logicGatesUpdated = logicGates.TryRemove(logicGate.Id, out _);
}
public void SimulationThread()
@@ -79,6 +86,12 @@ internal class LogicGateSimulator
{
tps++;
if (logicGatesUpdated)
{
logicGatesArray = logicGates.Values.ToArray();
logicGatesUpdated = false;
}
if (DateTime.Now > dateTime.AddSeconds(1))
{
ActualTicksPerSecond = tps;
@@ -0,0 +1,14 @@
using System;
namespace StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
[AttributeUsage(AttributeTargets.All)]
internal class LogicGateDescriptionAttribute : Attribute
{
public string Description { get; }
public LogicGateDescriptionAttribute(string description)
{
Description = description;
}
}
@@ -0,0 +1,14 @@
using System;
namespace StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
[AttributeUsage(AttributeTargets.Class)]
internal class LogicGateNameAttribute : Attribute
{
public string Name { get; }
public LogicGateNameAttribute(string name)
{
Name = name;
}
}
@@ -2,25 +2,23 @@
using MonoGame.Extended.Input;
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
[LogicGateName("Button")]
[LogicGateDescription("A button is a momentary switch that can be turned on and off.")]
internal class Button : LogicGate, IInteractable, IColorable
{
public override int OutputCount => 1;
public override int OutputCount { get; set; } = 1;
public override int InputCount => 0;
public override int InputCount { get; set; } = 0;
public bool IsPressed { get; set; }
public Color Color { get; set; } = Color.Purple;
public string Info { get; set; } = "OFF";
public Button()
{
Metadata.Name = "Button";
}
public void OnInteraction(MouseStateExtended mouseState, MouseStateExtended previousMouseState, KeyboardStateExtended keyboardStateExtended)
{
IsPressed = mouseState.IsButtonDown(MouseButton.Left);
@@ -1,18 +1,21 @@
using MonoGame.Extended.Input;
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using System;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
[LogicGateName("Clock")]
[LogicGateDescription("A clock is a circuit that oscillates between a high and a low state.")]
internal class Clock : LogicGate, IInteractable
{
private int count = 0;
private int tickRate = 0;
public override int OutputCount => 1;
public override int OutputCount { get; set; } = 1;
public override int InputCount => 0;
public override int InputCount { get; set; } = 0;
public string Info
{
@@ -27,11 +30,6 @@ internal class Clock : LogicGate, IInteractable
}
}
public Clock()
{
Metadata.Name = "Clock";
}
public void OnInteraction(MouseStateExtended mouseState, MouseStateExtended previousMouseState, KeyboardStateExtended keyboardStateExtended)
{
if (mouseState.DeltaScrollWheelValue == 0 || !keyboardStateExtended.IsShiftDown())
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
#pragma warning disable S112 // General exceptions should never be thrown
@@ -14,12 +15,12 @@ internal abstract class LogicGate
private int output;
private int cachedOutput;
public LogicGateMetadata Metadata { get; set; } = new LogicGateMetadata();
public int Id { get; set; }
public LogicGateWorldData WorldData { get; init; } = new LogicGateWorldData();
public ulong Id { get; internal set; }
public IReadOnlyList<LogicGateConnection> LogicGateConnections => logicGateConnections.AsReadOnly();
public abstract int OutputCount { get; }
public abstract int OutputCount { get; set; }
public abstract int InputCount { get; }
public abstract int InputCount { get; set; }
public void NextTick()
{
@@ -58,6 +59,13 @@ internal abstract class LogicGate
logicGateConnections.Add(new LogicGateConnection(logicGate, inputIndex, outputIndex));
}
public bool IsConnectedTo(LogicGate logicGate)
{
#pragma warning disable S6605 // Collection-specific "Exists" method should be used instead of the "Any" extension
return logicGateConnections.Any(c => c.LogicGate.Id == logicGate.Id);
#pragma warning restore S6605 // Collection-specific "Exists" method should be used instead of the "Any" extension
}
public void Disconnect(LogicGate logicGate)
{
int index = logicGateConnections.FindIndex(c => c.LogicGate.Id == logicGate.Id);
@@ -81,6 +89,17 @@ internal abstract class LogicGate
return cachedOutput.GetBit(index);
}
public override bool Equals(object? obj)
{
return obj is LogicGate gate &&
Id == gate.Id;
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
protected abstract void Execute();
protected int GetInputBit(int index)
@@ -1,14 +1,17 @@
using Microsoft.Xna.Framework;
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
[LogicGateName("Lamp")]
[LogicGateDescription("A lamp is a light source that can be turned on and off.")]
internal class Lamp : LogicGate, IColorable
{
public override int OutputCount => 0;
public override int OutputCount { get; set; } = 0;
public override int InputCount => 1;
public override int InputCount { get; set; } = 1;
public Color Color { get; set; } = Color.CadetBlue;
@@ -2,12 +2,10 @@
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
internal record LogicGateMetadata
internal record LogicGateWorldData
{
public Vector2 Position { get; set; }
public Vector2 Size { get; set; } = new Vector2(100, 100);
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
@@ -0,0 +1,18 @@
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
[LogicGateName("Not Gate")]
[LogicGateDescription("A not gate is a gate that inverts the input.")]
internal class NotGate : LogicGate
{
public override int InputCount { get; set; } = 1;
public override int OutputCount { get; set; } = 1;
protected override void Execute()
{
SetOutputBit(GetInputBit(0) == 1 ? 0 : 1, 0);
}
}
@@ -1,12 +1,15 @@
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
[LogicGateName("Pin")]
[LogicGateDescription("A pin is a input or output.")]
internal class Pin : LogicGate
{
public override int OutputCount => 1;
public override int OutputCount { get; set; } = 1;
public override int InputCount => 1;
public override int InputCount { get; set; } = 1;
protected override void Execute()
{
@@ -2,25 +2,23 @@
using MonoGame.Extended.Input;
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
[LogicGateName("Switch")]
[LogicGateDescription("A switch is a toggleable switch that can be turned on and off.")]
internal class Switch : LogicGate, IInteractable, IColorable
{
public override int OutputCount => 1;
public override int OutputCount { get; set; } = 1;
public override int InputCount => 0;
public override int InputCount { get; set; } = 0;
public bool IsPressed { get; set; }
public Color Color { get; set; } = Color.Purple;
public string Info { get; set; } = "OFF";
public Switch()
{
Metadata.Name = "Switch";
}
public void OnInteraction(MouseStateExtended mouseState, MouseStateExtended previousMouseState, KeyboardStateExtended keyboardStateExtended)
{
if (mouseState.IsButtonDown(MouseButton.Left) && !previousMouseState.IsButtonDown(MouseButton.Left))
@@ -1,20 +0,0 @@
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
namespace StoneRed.LogicSimulator.Simulation.LogicGates;
internal class TestLogicGate : LogicGate
{
public override int InputCount => 1;
public override int OutputCount => 1;
public TestLogicGate()
{
Metadata.Name = "Test";
}
protected override void Execute()
{
SetOutputBit(GetInputBit(0) == 1 ? 0 : 1, 0);
}
}
+12 -8
View File
@@ -2,7 +2,6 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Screens;
@@ -33,6 +32,8 @@ internal class Srls : Game
public FontSystem FontSystem { get; private set; } = null!;
public AssetManager AssetManager { get; private set; } = null!;
public LogicGatesManager LogicGatesManager { get; private set; } = null!;
public Settings Settings { get; set; }
public string ContentPath { get; }
@@ -98,13 +99,17 @@ internal class Srls : Game
currentSrlsWindow = window;
}
public void ShowContextMenu(string title, Point position, params MenuItem[] menuItems)
public TextButton ShowContextMenu(string title, Point position, MenuItem[] menuItems, bool showButton = false)
{
string data = File.ReadAllText(Path.Combine(ContentPath, "ContextMenu.xmmp"));
VerticalStackPanel contextMenu = (VerticalStackPanel)Project.LoadFromXml(data, AssetManager).Root;
contextMenu.FindChildById<Label>("title").Text = title;
TextButton button = contextMenu.FindChildById<TextButton>("button");
button.Visible = showButton;
VerticalMenu menu = contextMenu.FindChildById<VerticalMenu>("menu");
foreach (MenuItem menuItem in menuItems)
@@ -114,6 +119,8 @@ internal class Srls : Game
Desktop.ShowContextMenu(contextMenu, position);
Desktop.ContextMenu.Scale = new Vector2(Scale / 3, Scale / 3);
return button;
}
protected override void Update(GameTime gameTime)
@@ -131,12 +138,6 @@ internal class Srls : Game
currentSrlsWindow.Scale = currentSrlsWindow.ScalingEnabled ? new Vector2(Scale / 2, Scale / 2) : Vector2.One;
}
KeyboardState keyboardState = Keyboard.GetState();
if (keyboardState.IsKeyDown(Keys.Q))
{
ShowWindow<QuickMenu>();
}
base.Update(gameTime);
}
@@ -168,6 +169,9 @@ internal class Srls : Game
FileAssetResolver assetResolver = new FileAssetResolver(ContentPath);
AssetManager = new AssetManager(assetResolver);
LogicGatesManager = new LogicGatesManager();
LogicGatesManager.LoadLogicGates();
LoadScreen<StartScreen>();
base.Initialize();
@@ -20,6 +20,7 @@
<EmbeddedResource Include="Icon.bmp" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentResults" Version="3.15.2" />
<PackageReference Include="MonoGame.Extended" Version="3.8.0" />
<PackageReference Include="MonoGame.Extended.Entities" Version="3.8.0" />
<PackageReference Include="MonoGame.Extended.Graphics" Version="3.8.0" />
@@ -1,23 +1,32 @@
using Microsoft.Xna.Framework;
using FluentResults;
using Microsoft.Xna.Framework;
using Myra.Graphics2D.UI;
using StoneRed.LogicSimulator.Simulation.LogicGates;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using StoneRed.LogicSimulator.WorldSaveSystem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.UserInterface.Screens;
internal class LoadingScreen : SrlsScreen<VerticalStackPanel>
{
private readonly string filePath;
private Label label = null!;
private HorizontalProgressBar horizontalProgressBar = null!;
protected override string XmmpPath => "LoadingScreen.xmmp";
public LoadingScreen(string filePath)
{
this.filePath = filePath;
}
protected override void Initialize()
{
label = Root.FindChildById<Label>("label");
@@ -27,25 +36,32 @@ internal class LoadingScreen : SrlsScreen<VerticalStackPanel>
protected override void LoadContent()
{
Progress<WorldSaveLoadProgress> progress = new Progress<WorldSaveLoadProgress>();
progress.ProgressChanged += Progress_ProgressChanged;
WorldLoader worldLoader = new WorldLoader(srls);
_ = Task.Run(async () =>
{
for (int i = 0; i < 100; i++)
Result<IEnumerable<LogicGate>> result = await worldLoader.LoadWorld(filePath, progress);
if (result.IsFailed)
{
horizontalProgressBar.Value++;
await Task.Delay(1);
Dialog.CreateMessageBox("Error", string.Join(',', result.Errors.Select(e => e.Message))).Show(srls.Desktop);
}
});
}
protected override void Update(GameTime gameTime)
{
return;
label.Text = $"Loading... {Math.Round(100 / horizontalProgressBar.Maximum * horizontalProgressBar.Value, 0)}%";
if (horizontalProgressBar.Value >= horizontalProgressBar.Maximum)
{
Clock button = new Clock()
{
Id = 0,
Metadata = new LogicGateMetadata()
WorldData = new LogicGateWorldData()
{
Name = "Clock",
Position = new Vector2(0, 0)
@@ -55,50 +71,50 @@ internal class LoadingScreen : SrlsScreen<VerticalStackPanel>
Switch @switch = new Switch()
{
Id = 1,
Metadata = new LogicGateMetadata()
WorldData = new LogicGateWorldData()
{
Name = "Switch",
Position = new Vector2(0, 150)
Position = new Vector2(0, 200)
}
};
TestLogicGate testLogicGate1 = new TestLogicGate()
NotGate testLogicGate1 = new NotGate()
{
Id = 2,
Metadata = new LogicGateMetadata()
WorldData = new LogicGateWorldData()
{
Name = "L1",
Position = new Vector2(150, 150)
Name = "Not Gate",
Position = new Vector2(200, 200)
}
};
Pin testLogicGate2 = new Pin()
{
Id = 3,
Metadata = new LogicGateMetadata()
WorldData = new LogicGateWorldData()
{
Name = "Pin",
Position = new Vector2(300, 300)
Position = new Vector2(400, 400)
}
};
Pin testLogicGate3 = new Pin()
{
Id = 4,
Metadata = new LogicGateMetadata()
WorldData = new LogicGateWorldData()
{
Name = "Pin",
Position = new Vector2(450, 450)
Position = new Vector2(500, 500)
}
};
Lamp lamp = new Lamp()
{
Id = 5,
Metadata = new LogicGateMetadata()
WorldData = new LogicGateWorldData()
{
Name = "Lamp",
Position = new Vector2(600, 450)
Position = new Vector2(600, 600)
}
};
@@ -125,4 +141,10 @@ internal class LoadingScreen : SrlsScreen<VerticalStackPanel>
protected override void Draw(GameTime gameTime)
{
}
private void Progress_ProgressChanged(object? sender, WorldSaveLoadProgress e)
{
horizontalProgressBar.Value = e.Percentage;
label.Text = e.Message;
}
}
@@ -58,6 +58,9 @@ internal abstract class SrlsScreen
public event EventHandler<GameScreenWrapperEventArgs>? OnDraw;
private bool initialized;
private bool loadedContent;
public GameScreenWrapper(Game game) : base(game)
{
}
@@ -69,13 +72,21 @@ internal abstract class SrlsScreen
public override void Initialize()
{
if (!initialized)
{
initialized = true;
OnInitialize?.Invoke(this, EventArgs.Empty);
}
}
public override void LoadContent()
{
if (!loadedContent)
{
loadedContent = true;
OnLoadContent?.Invoke(this, EventArgs.Empty);
}
}
public override void UnloadContent()
{
@@ -1,22 +1,25 @@
using Microsoft.Xna.Framework;
using Myra.Graphics2D.UI;
using Myra.Graphics2D.UI.File;
using StoneRed.LogicSimulator.UserInterface.Windows;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.UserInterface.Screens;
internal class StartScreen : SrlsScreen<VerticalStackPanel>
{
protected override string XmmpPath => "StartScreen.xmmp";
protected override void LoadContent()
protected override void Initialize()
{
VerticalMenu menu = Root.FindChildById<VerticalMenu>("menu");
menu.FindMenuItemById("menuQuit").Selected += (s, a) => srls.Exit();
menu.FindMenuItemById("menuSettings").Selected += (s, a) => srls.ShowWindow<SettingsWindow>();
menu.FindMenuItemById("menuStartNew").Selected += (s, a) => srls.LoadScreen<WorldScreen>();
menu.FindMenuItemById("menuLoad").Selected += (s, a) => srls.LoadScreen(new LoadingScreen());
menu.FindMenuItemById("menuLoad").Selected += MenuLoad_Selected;
}
protected override void Draw(GameTime gameTime)
@@ -26,4 +29,24 @@ internal class StartScreen : SrlsScreen<VerticalStackPanel>
protected override void Update(GameTime gameTime)
{
}
private void MenuLoad_Selected(object? sender, System.EventArgs e)
{
FileDialog fileDialog = new FileDialog(FileDialogMode.OpenFile);
fileDialog.Show(srls.Desktop);
fileDialog.Scale = new Vector2(srls.Scale / 3);
fileDialog.Closed += async (s, a) =>
{
if (!fileDialog.Result)
{
return;
}
// Very cheap workaround to make sure the file dialog is closed before loading the screen
await Task.Delay(100);
srls.LoadScreen(new LoadingScreen(fileDialog.FilePath));
};
}
}
@@ -11,9 +11,12 @@ using Myra.Graphics2D.UI;
using StoneRed.LogicSimulator.Simulation;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using StoneRed.LogicSimulator.UserInterface.Windows;
using StoneRed.LogicSimulator.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using IColorable = StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces.IColorable;
@@ -22,26 +25,34 @@ namespace StoneRed.LogicSimulator.UserInterface.Screens;
internal class WorldScreen : SrlsScreen<Grid>
{
private readonly LogicGateSimulator simulator;
private readonly Vector2 logicGateSize = new Vector2(100, 100);
private OrthographicCamera camera = null!;
private RichTextLayout richTextLayout = null!;
private Label fpsLabel = null!;
private Label upsLabel = null!;
private Label positionLabel = null!;
private SpinButton frequency = null!;
private CheckBox highPerformance = null!;
private ListBox nativeComponentsListBox = null!;
private float fps;
private MouseStateExtended previousMouseState;
public override bool ScalingEnabled => false;
private ConnectionContext? connectionContext = null;
private LogicGate? selectedLogicGate = null;
public override bool ScalingEnabled => false;
protected override string XmmpPath => "WorldScreen.xmmp";
public WorldScreen(List<LogicGate> logicGates)
public WorldScreen(IEnumerable<LogicGate> logicGates)
{
simulator = new LogicGateSimulator(logicGates);
}
public WorldScreen()
{
simulator = new LogicGateSimulator(new List<LogicGate>());
simulator = new LogicGateSimulator(Enumerable.Empty<LogicGate>());
}
protected override void Initialize()
@@ -49,11 +60,26 @@ internal class WorldScreen : SrlsScreen<Grid>
VerticalStackPanel infoPanel = Root.FindChildById<VerticalStackPanel>("info");
fpsLabel = infoPanel.FindChildById<Label>("fps");
upsLabel = infoPanel.FindChildById<Label>("ups");
positionLabel = infoPanel.FindChildById<Label>("position");
VerticalStackPanel settingsPanel = Root.FindChildById<VerticalStackPanel>("settings");
frequency = settingsPanel.FindChildById<SpinButton>("frequency");
highPerformance = settingsPanel.FindChildById<CheckBox>("highPerformance");
nativeComponentsListBox = Root.FindChildById<ListBox>("nativeComponents");
foreach (LogicGateInfo logicGateInfo in srls.LogicGatesManager.GetNativeLogicGatesInfos())
{
nativeComponentsListBox.Items.Add(new ListItem(logicGateInfo.TypeName));
}
nativeComponentsListBox.SelectedIndexChanged += NativeComponentsListBox_SelectedIndexChanged;
richTextLayout = new RichTextLayout
{
Font = srls.FontSystem.GetFont(15)
};
camera = new OrthographicCamera(srls.GraphicsDevice)
{
MinimumZoom = 0.1f,
@@ -68,11 +94,6 @@ internal class WorldScreen : SrlsScreen<Grid>
{
fps = 1f / gameTime.GetElapsedSeconds();
RichTextLayout richTextLayout = new RichTextLayout
{
Font = srls.FontSystem.GetFont(15)
};
Matrix transformMatrix = camera.GetViewMatrix();
srls.SpriteBatch.Begin(SpriteSortMode.BackToFront, transformMatrix: transformMatrix);
@@ -87,22 +108,37 @@ internal class WorldScreen : SrlsScreen<Grid>
if (logicGate is IInteractable interactable)
{
richTextLayout.Text = interactable.Info;
richTextLayout.Draw(srls.SpriteBatch, (new Vector2(0, 15) * srls.Scale) + (logicGate.Metadata.Position * srls.Scale), Color.Black, new Vector2(srls.Scale, srls.Scale), layerDepth: 0);
richTextLayout.Draw(srls.SpriteBatch, (new Vector2(0, 15) * srls.Scale) + (logicGate.WorldData.Position * srls.Scale), Color.Black, new Vector2(srls.Scale, srls.Scale), layerDepth: 0);
}
richTextLayout.Text = logicGate.Metadata.Name;
// Draw text for components
richTextLayout.Text = logicGate.WorldData.Name;
richTextLayout.Draw(srls.SpriteBatch, logicGate.WorldData.Position * srls.Scale, Color.Black, new Vector2(srls.Scale, srls.Scale), layerDepth: 0);
richTextLayout.Draw(srls.SpriteBatch, logicGate.Metadata.Position * srls.Scale, Color.Black, new Vector2(srls.Scale, srls.Scale), layerDepth: 0);
srls.SpriteBatch.FillRectangle(logicGate.Metadata.Position * srls.Scale, logicGate.Metadata.Size * srls.Scale, color, 0.2f);
// Draw logic gate
srls.SpriteBatch.FillRectangle(logicGate.WorldData.Position * srls.Scale, logicGateSize * srls.Scale, color, 0.2f);
// Draw logic gate connections
foreach (LogicGateConnection connection in logicGate.LogicGateConnections)
{
Color lineColor = logicGate.GetCachedOutputBit(connection.OutputIndex) == 1 ? Color.Red : Color.LightBlue;
srls.SpriteBatch.DrawLine((logicGate.Metadata.Position * srls.Scale) + (logicGate.Metadata.Size / 2 * srls.Scale), (connection.LogicGate.Metadata.Position * srls.Scale) + (logicGate.Metadata.Size / 2 * srls.Scale), lineColor, 5 * srls.Scale, 0.1f);
srls.SpriteBatch.DrawLine((logicGate.WorldData.Position * srls.Scale) + (logicGateSize / 2 * srls.Scale), (connection.LogicGate.WorldData.Position * srls.Scale) + (logicGateSize / 2 * srls.Scale), lineColor, 5 * srls.Scale, 0.1f);
}
}
if (connectionContext is not null)
{
LogicGate logicGate = connectionContext.LogicGate;
srls.SpriteBatch.DrawLine((logicGate.WorldData.Position * srls.Scale) + (logicGateSize / 2 * srls.Scale), camera.ScreenToWorld(previousMouseState.Position.ToVector2()), Color.Purple, 5 * srls.Scale, 0.1f);
}
if (selectedLogicGate is not null)
{
richTextLayout.Text = selectedLogicGate.WorldData.Name;
richTextLayout.Draw(srls.SpriteBatch, selectedLogicGate.WorldData.Position, Color.White, new Vector2(srls.Scale, srls.Scale), layerDepth: 0);
srls.SpriteBatch.DrawRectangle(selectedLogicGate.WorldData.Position, logicGateSize * srls.Scale, Color.Red, 2 * srls.Scale);
}
srls.SpriteBatch.End();
}
@@ -125,12 +161,15 @@ internal class WorldScreen : SrlsScreen<Grid>
}
fpsLabel.Text = $"FPS: {Math.Round(fps)}";
upsLabel.Text = $"FRQ: {CalculateFrequency(simulator.ActualTicksPerSecond)}/{CalculateFrequency(simulator.TargetTicksPerSecond)}{(simulator.HighPerformanceClock ? "*" : string.Empty)} {(simulator.ClockCalibrating ? $"[Calibrating... {calibrationPercentage}%]" : string.Empty)}";
upsLabel.Text = $"FRQ: {FrequencyCalculator.CalculateFrequency(simulator.ActualTicksPerSecond)}/{FrequencyCalculator.CalculateFrequency(simulator.TargetTicksPerSecond)}{(simulator.HighPerformanceClock ? "*" : string.Empty)} {(simulator.ClockCalibrating ? $"[Calibrating... {calibrationPercentage}%]" : string.Empty)}";
positionLabel.Text = $"X/Y: {(long)Math.Round(camera.Position.X / logicGateSize.X / srls.Scale)}/{(long)Math.Round(camera.Position.Y / logicGateSize.Y / srls.Scale)}";
simulator.TargetTicksPerSecond = (int)frequency.Value.GetValueOrDefault();
simulator.HighPerformanceClock = highPerformance.IsChecked;
fpsLabel.Font = srls.FontSystem.GetFont(10 * srls.Scale);
upsLabel.Font = srls.FontSystem.GetFont(10 * srls.Scale);
positionLabel.Font = srls.FontSystem.GetFont(10 * srls.Scale);
MouseStateExtended mouseState = MouseExtended.GetState();
KeyboardStateExtended keyboardState = KeyboardExtended.GetState();
@@ -139,14 +178,18 @@ internal class WorldScreen : SrlsScreen<Grid>
foreach (LogicGate logicGate in simulator.GetLogicGates())
{
Rectangle rectangle = new Rectangle((logicGate.Metadata.Position * srls.Scale).ToPoint(), (logicGate.Metadata.Size * srls.Scale).ToPoint());
Rectangle rectangle = new Rectangle((logicGate.WorldData.Position * srls.Scale).ToPoint(), (logicGateSize * srls.Scale).ToPoint());
if (rectangle.Contains(camera.ScreenToWorld(mouseState.Position.ToVector2())))
{
if (mouseState.IsButtonDown(MouseButton.Right))
if (mouseState.IsButtonDown(MouseButton.Right) && selectedLogicGate is null)
{
ShowConnectionContextMenu(logicGate, mouseState.Position, keyboardState.IsShiftDown());
}
else if (!srls.Desktop.IsMouseOverGUI && keyboardState.IsKeyDown(Keys.X) && connectionContext is null)
{
simulator.RemoveLogicGate(logicGate);
}
else if (!srls.Desktop.IsMouseOverGUI && logicGate is IInteractable interactable)
{
interactable.OnInteraction(mouseState, previousMouseState, keyboardState);
@@ -161,13 +204,42 @@ internal class WorldScreen : SrlsScreen<Grid>
srls.Desktop.HideContextMenu();
}
if (selectedLogicGate is not null)
{
if (mouseState.IsButtonDown(MouseButton.Left) && !mouseOverGate && !srls.Desktop.IsMouseOverGUI)
{
selectedLogicGate.WorldData.Position /= srls.Scale;
simulator.AddLogicGate(selectedLogicGate);
selectedLogicGate = null;
nativeComponentsListBox.SelectedIndex = -1;
}
else
{
Vector2 position = camera.ScreenToWorld(mouseState.Position.ToVector2());
position = new Vector2(position.X - RealMod(position.X, 100 * srls.Scale), position.Y - RealMod(position.Y, 100 * srls.Scale));
selectedLogicGate.WorldData.Position = position;
}
}
if (keyboardState.IsKeyDown(Keys.C))
{
connectionContext = null;
selectedLogicGate = null;
}
if (keyboardState.IsKeyDown(Keys.Q))
{
srls.ShowWindow<QuickMenu>();
}
previousMouseState = mouseState;
float movementSpeed = (float)Math.Pow(200, 2 - camera.Zoom);
movementSpeed = Math.Clamp(movementSpeed, 1000, 20000);
camera.Move(GetMovementDirection(keyboardState) * movementSpeed * gameTime.GetElapsedSeconds());
camera.Move(keyboardState.GetMovementDirection() * movementSpeed * gameTime.GetElapsedSeconds());
if (!keyboardState.IsShiftDown())
{
@@ -187,64 +259,131 @@ internal class WorldScreen : SrlsScreen<Grid>
simulator.Stop();
}
private static Vector2 GetMovementDirection(KeyboardStateExtended keyboardState)
private static float RealMod(float x, float m)
{
Vector2 movementDirection = Vector2.Zero;
if (keyboardState.IsKeyDown(Keys.S))
{
movementDirection += Vector2.UnitY;
}
if (keyboardState.IsKeyDown(Keys.W))
{
movementDirection -= Vector2.UnitY;
}
if (keyboardState.IsKeyDown(Keys.A))
{
movementDirection -= Vector2.UnitX;
}
if (keyboardState.IsKeyDown(Keys.D))
{
movementDirection += Vector2.UnitX;
}
return movementDirection;
float r = x % m;
return r < 0 ? r + m : r;
}
private static string CalculateFrequency(double hz)
private void NativeComponentsListBox_SelectedIndexChanged(object? sender, EventArgs e)
{
double khz = hz / 1000d;
double mhz = khz / 1000d;
double ghz = mhz / 1000d;
if (ghz >= 1)
if (nativeComponentsListBox.SelectedItem is not null)
{
return $"{Math.Round(ghz)}GHz";
}
else if (mhz >= 1)
{
return $"{Math.Round(mhz)}MHz";
}
else if (khz >= 1)
{
return $"{Math.Round(khz)}kHz";
}
else
{
return $"{Math.Round(hz)}Hz";
connectionContext = null;
selectedLogicGate = srls.LogicGatesManager.CreateLogicGate(nativeComponentsListBox.SelectedItem.Text);
selectedLogicGate.WorldData.Name = nativeComponentsListBox.SelectedItem.Text;
}
}
private void ShowConnectionContextMenu(LogicGate logicGate, Point position, bool showInputs)
{
showInputs = logicGate.OutputCount <= 0 || (showInputs && logicGate.InputCount > 0);
string buttonText = "Settings";
if (connectionContext is not null)
{
showInputs = !connectionContext.IsInput;
buttonText = "Cancel";
}
int count = showInputs ? logicGate.InputCount : logicGate.OutputCount;
MenuItem[] menuItems = new MenuItem[count];
for (int i = 0; i < count; i++)
{
menuItems[i] = new MenuItem(i.ToString(), showInputs ? $"Input {i}" : $"Output {i}");
int index = i;
string connectionStatus = string.Empty;
if (connectionContext is not null)
{
bool isConnected = false;
if (connectionContext.IsInput)
{
isConnected = logicGate.IsConnectedTo(connectionContext.LogicGate);
}
else
{
isConnected = connectionContext.LogicGate.IsConnectedTo(logicGate);
}
srls.ShowContextMenu(logicGate.Metadata.Name, position, menuItems);
connectionStatus = isConnected ? "[Disconnect]" : "[Connect]";
}
menuItems[i] = new MenuItem(i.ToString(), $"{connectionStatus} " + (showInputs ? $"Input {i}" : $"Output {i}"));
menuItems[i].Selected += (_, _) => OnConnectionClicked(logicGate, index, showInputs);
}
TextButton button = srls.ShowContextMenu(logicGate.WorldData.Name, position, menuItems, true);
button.Text = buttonText;
button.Click += (_, _) =>
{
srls.Desktop.HideContextMenu();
if (connectionContext is not null)
{
connectionContext = null;
}
else
{
// Show settings window
}
};
}
private void OnConnectionClicked(LogicGate logicGate, int index, bool isInput)
{
if (connectionContext is null)
{
connectionContext = new ConnectionContext(logicGate)
{
Index = index,
IsInput = isInput
};
return;
}
if (connectionContext.IsInput)
{
if (logicGate.IsConnectedTo(connectionContext.LogicGate))
{
logicGate.Disconnect(connectionContext.LogicGate);
}
else
{
logicGate.Connect(connectionContext.LogicGate, connectionContext.Index, index);
}
}
else
{
if (connectionContext.LogicGate.IsConnectedTo(logicGate))
{
connectionContext.LogicGate.Disconnect(logicGate);
}
else
{
connectionContext.LogicGate.Connect(logicGate, index, connectionContext.Index);
}
}
connectionContext = null;
}
private sealed class ConnectionContext
{
public bool IsInput { get; set; }
public LogicGate LogicGate { get; set; }
public int Index { get; set; }
public ConnectionContext(LogicGate logicGate)
{
LogicGate = logicGate;
}
}
}
@@ -1,4 +1,5 @@
using Myra.Graphics2D.UI;
using StoneRed.LogicSimulator.UserInterface.Screens;
namespace StoneRed.LogicSimulator.UserInterface.Windows;
@@ -13,6 +14,8 @@ internal class QuickMenu : SrlsWindow
menu.FindMenuItemById("menuMainMenu").Selected += (s, a) => srls.LoadScreen<StartScreen>();
menu.FindMenuItemById("menuSettings").Selected += (s, a) => srls.ShowWindow<SettingsWindow>();
menu.FindMenuItemById("menuSave").Selected += (s, a) => srls.ShowWindow<SettingsWindow>();
menu.FindMenuItemById("menuLoad").Selected += (s, a) => srls.ShowWindow<SettingsWindow>();
menu.FindMenuItemById("menuQuit").Selected += QuickMenu_Selected;
}
@@ -0,0 +1,30 @@
using System;
namespace StoneRed.LogicSimulator.Utilities;
internal static class FrequencyCalculator
{
public static string CalculateFrequency(double hz)
{
double khz = hz / 1000d;
double mhz = khz / 1000d;
double ghz = mhz / 1000d;
if (ghz >= 1)
{
return $"{Math.Round(ghz)}GHz";
}
else if (mhz >= 1)
{
return $"{Math.Round(mhz)}MHz";
}
else if (khz >= 1)
{
return $"{Math.Round(khz)}kHz";
}
else
{
return $"{Math.Round(hz)}Hz";
}
}
}
@@ -0,0 +1,31 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended.Input;
namespace StoneRed.LogicSimulator.Utilities;
internal static class InputHelper
{
public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState)
{
Vector2 movementDirection = Vector2.Zero;
if (keyboardState.IsKeyDown(Keys.S))
{
movementDirection += Vector2.UnitY;
}
if (keyboardState.IsKeyDown(Keys.W))
{
movementDirection -= Vector2.UnitY;
}
if (keyboardState.IsKeyDown(Keys.A))
{
movementDirection -= Vector2.UnitX;
}
if (keyboardState.IsKeyDown(Keys.D))
{
movementDirection += Vector2.UnitX;
}
return movementDirection;
}
}
@@ -0,0 +1,108 @@
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StoneRed.LogicSimulator.Utilities;
internal class LogicGatesManager
{
private readonly Dictionary<LogicGateInfo, Type> logicGates = new Dictionary<LogicGateInfo, Type>();
public void LoadLogicGates()
{
IEnumerable<Type> logicGateTypes = typeof(LogicGate)
.Assembly.GetTypes()
.Where(t => t.IsSubclassOf(typeof(LogicGate)) && !t.IsAbstract);
foreach (Type type in logicGateTypes)
{
LogicGateDescriptionAttribute? descriptionAttribute = type.GetCustomAttributes(typeof(LogicGateDescriptionAttribute), false).FirstOrDefault() as LogicGateDescriptionAttribute;
if (type.GetCustomAttributes(typeof(LogicGateNameAttribute), false).FirstOrDefault() is LogicGateNameAttribute nameAttribute)
{
LogicGateInfo logicGateInfo = new LogicGateInfo(nameAttribute.Name, descriptionAttribute?.Description);
if (logicGates.ContainsKey(logicGateInfo))
{
logicGateInfo.TypeName += "#";
}
logicGates.Add(logicGateInfo, type);
}
}
}
public IEnumerable<LogicGateInfo> GetNativeLogicGatesInfos()
{
return logicGates.Keys;
}
public LogicGate CreateLogicGate(string typeName)
{
Type type = logicGates[new LogicGateInfo(typeName, null)];
return (LogicGate)Activator.CreateInstance(type)!;
}
public string GetTypeName(Type type)
{
LogicGateNameAttribute nameAttribute = (LogicGateNameAttribute)type.GetCustomAttributes(typeof(LogicGateNameAttribute), false)[0];
return nameAttribute.Name;
}
public bool TryGetTypeName(Type type, [NotNullWhen(true)] out string? typeName)
{
if (type.GetCustomAttributes(typeof(LogicGateNameAttribute), false).FirstOrDefault() is not LogicGateNameAttribute nameAttribute)
{
typeName = null;
return false;
}
typeName = nameAttribute.Name;
return true;
}
public bool TryCreateLogicGate(string typeName, [NotNullWhen(true)] out LogicGate? logicGate)
{
if (!logicGates.ContainsKey(new LogicGateInfo(typeName, null)))
{
logicGate = null;
return false;
}
Type type = logicGates[new LogicGateInfo(typeName, null)];
logicGate = (LogicGate)Activator.CreateInstance(type)!;
return true;
}
}
internal class LogicGateInfo
{
public string TypeName { get; set; }
public string? Description { get; set; }
public LogicGateInfo(string name, string? description)
{
TypeName = name;
Description = description;
}
public override int GetHashCode()
{
return TypeName.GetHashCode();
}
public override bool Equals(object? obj)
{
if (obj is LogicGateInfo logicGateInfo)
{
return logicGateInfo.TypeName == TypeName;
}
return false;
}
}
@@ -0,0 +1,41 @@
using FluentResults;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using StoneRed.LogicSimulator.WorldSaveSystem.WorldReaders;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.WorldSaveSystem;
internal class WorldLoader
{
private readonly Srls srls;
public WorldLoader(Srls srls)
{
this.srls = srls;
}
public Task<Result<IEnumerable<LogicGate>>> LoadWorld(string filePath, IProgress<WorldSaveLoadProgress> progress)
{
BinaryReader binaryReader = new BinaryReader(File.OpenRead(filePath));
ushort fileVersion = binaryReader.ReadUInt16();
binaryReader.Close();
IWorldReader? worldReader = fileVersion switch
{
1 => new WorldReaderV1(srls),
_ => null
};
if (worldReader is null)
{
return Task.FromResult(Result.Fail<IEnumerable<LogicGate>>("Invalid file version!"));
}
return worldReader.ReadWorld(filePath, progress);
}
}
@@ -0,0 +1,14 @@
using FluentResults;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.WorldSaveSystem.WorldReaders;
internal interface IWorldReader
{
Task<Result<IEnumerable<LogicGate>>> ReadWorld(string filePath, IProgress<WorldSaveLoadProgress> progress);
}
@@ -0,0 +1,113 @@
using FluentResults;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.WorldSaveSystem.WorldReaders;
internal class WorldReaderV1 : IWorldReader
{
private readonly Srls srls;
public WorldReaderV1(Srls srls)
{
this.srls = srls;
}
public async Task<Result<IEnumerable<LogicGate>>> ReadWorld(string filePath, IProgress<WorldSaveLoadProgress> progress)
{
if (!File.Exists(filePath))
{
return Result.Fail($"File \"{filePath}\" does not exist");
}
return await Task.Run(() => InternalReadWorld(filePath, progress));
}
public Result<IEnumerable<LogicGate>> InternalReadWorld(string filePath, IProgress<WorldSaveLoadProgress> progress)
{
BinaryReader? reader = null;
Dictionary<LogicGate, List<(ulong GateRefId, int inputIndex, int outputIndex)>> connections = new();
try
{
progress.Report(new(0, "Opening file"));
reader = new BinaryReader(File.OpenRead(filePath));
_ = reader.ReadUInt16(); // File version
int numberOfLogicGates = reader.ReadInt32();
for (int gateNumber = 0; gateNumber < numberOfLogicGates; gateNumber++)
{
progress.Report(new((int)((double)gateNumber / numberOfLogicGates * 50d), $"Loading logic gates ({gateNumber / numberOfLogicGates})"));
ulong id = reader.ReadUInt64();
string typeName = reader.ReadString();
int inputCount = reader.ReadInt32();
int outputCount = reader.ReadInt32();
int numberOfConnections = reader.ReadInt32();
if (srls.LogicGatesManager.TryCreateLogicGate(typeName, out LogicGate? logicGate))
{
logicGate.Id = id;
logicGate.InputCount = inputCount;
logicGate.OutputCount = outputCount;
}
else
{
return Result.Fail($"Logic gate type \"{typeName}\" does not exist!");
}
connections.Add(logicGate, new());
for (int connectionNumber = 0; connectionNumber < numberOfConnections; connectionNumber++)
{
ulong gateRefId = reader.ReadUInt64();
int inputIndex = reader.ReadInt32();
int outputIndex = reader.ReadInt32();
connections[logicGate].Add((gateRefId, inputIndex, outputIndex));
}
}
int connectionCount = 0;
foreach (KeyValuePair<LogicGate, List<(ulong gateRefId, int inputIndex, int outputIndex)>> connectionPair in connections)
{
progress.Report(new((int)((double)connections.Count / connectionCount * 50d), $"Connecting logic gates ({connections.Count / connectionCount})"));
LogicGate logicGate = connectionPair.Key;
foreach ((ulong gateRefId, int inputIndex, int outputIndex) in connectionPair.Value)
{
LogicGate? connectedGate = connections.Keys.FirstOrDefault(g => g.Id == gateRefId);
if (connectedGate is null)
{
return Result.Fail($"Logic gate connection {logicGate.Id} -> {gateRefId} does not exist!");
}
logicGate.Connect(connectedGate, inputIndex, outputIndex);
}
connectionCount++;
}
}
catch (IOException ex)
{
return Result.Fail(ex.Message);
}
finally
{
reader?.Dispose();
}
return connections.Keys;
}
}
@@ -0,0 +1,14 @@
namespace StoneRed.LogicSimulator.WorldSaveSystem;
internal class WorldSaveLoadProgress
{
public int Percentage { get; }
public string Message { get; }
public WorldSaveLoadProgress(int percentage, string message)
{
Percentage = percentage;
Message = message;
}
}
@@ -0,0 +1,5 @@
namespace StoneRed.LogicSimulator.WorldSaveSystem;
internal class WorldSaver
{
}
@@ -0,0 +1,14 @@
using FluentResults;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.WorldSaveSystem.WorldWriters;
internal interface IWorldWriter
{
Task<Result> WriteWorld(string filePath, IEnumerable<LogicGate> logicGates, IProgress<WorldSaveLoadProgress> progress);
}
@@ -0,0 +1,76 @@
using FluentResults;
using StoneRed.LogicSimulator.Simulation.LogicGates.Attributes;
using StoneRed.LogicSimulator.Simulation.LogicGates.Interfaces;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace StoneRed.LogicSimulator.WorldSaveSystem.WorldWriters;
internal class WorldWriterV1 : IWorldWriter
{
private readonly Srls srls;
public WorldWriterV1(Srls srls)
{
this.srls = srls;
}
public Task<Result> WriteWorld(string filePath, IEnumerable<LogicGate> logicGates, IProgress<WorldSaveLoadProgress> progress)
{
return Task.Run(() => InternalWriteWorld(filePath, logicGates, progress));
}
private Result InternalWriteWorld(string filePath, IEnumerable<LogicGate> logicGates, IProgress<WorldSaveLoadProgress> progress)
{
BinaryWriter? writer = null;
try
{
writer = new BinaryWriter(File.OpenWrite(filePath));
progress.Report(new(0, "Counting logic gates"));
int numberOfLogicGates = logicGates.Count();
writer.Write(1); // Write file version
writer.Write(numberOfLogicGates);
foreach (LogicGate logicGate in logicGates)
{
writer.Write(logicGate.Id);
if (!srls.LogicGatesManager.TryGetTypeName(logicGate.GetType(), out string? typeName))
{
return Result.Fail($"Logic gate type \"{logicGate.GetType().FullName}\" has no \"{nameof(LogicGateNameAttribute)}\" attribute!");
}
writer.Write(typeName);
writer.Write(logicGate.InputCount);
writer.Write(logicGate.OutputCount);
writer.Write(logicGate.LogicGateConnections.Count);
foreach (LogicGateConnection connection in logicGate.LogicGateConnections)
{
writer.Write(connection.LogicGate.Id);
writer.Write(connection.InputIndex);
writer.Write(connection.OutputIndex);
}
}
}
catch (IOException ex)
{
return Result.Fail(ex.Message);
}
finally
{
writer?.Dispose();
}
return Result.Ok();
}
}