Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Docs/Architecture/Key use cases of features/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
The purpose of this folder is to list the key use cases of the features in Yafc.
This information justifies why a feature is present in the first place, and helps to keep it relevant when changing it.
The purpose of this folder is to list the key use cases of the features in Yafc.
This information justifies why a feature is present in the first place, and helps to keep it relevant when changing it.
Not all features are listed - the feature is added here mostly if its usefulness was questioned.
8 changes: 4 additions & 4 deletions Docs/Architecture/Key use cases of features/UndoRedo.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
The Undo and Redo functionality has the following key use cases where it drastically improves a common operation:
The Undo and Redo functionality has the following key use cases where it drastically improves a common operation:

Key use case:
*Helping to fix a silently broken sheet*. The sheet is silently broken when there is no explicit error but the result is unreasonable due to linking or not specifying overproduction. It can be made worse by one noticing it only further down the line, when it's not obvious what broke it. Without Undo, fixing a large sheet may require rebuilding whole sections from scratch. The larger the sheet the harder it is to debug because one is not told clearly what the solver breaks on. In this scenario, having Undo would save a lot of time by pointing out where exactly the sheet broke.
Key use case:
*Helping to fix a silently broken sheet*. The sheet is silently broken when there is no explicit error but the result is unreasonable due to linking or not specifying overproduction. It can be made worse by one noticing it only further down the line, when it's not obvious what broke it. Without Undo, fixing a large sheet may require rebuilding whole sections from scratch. The larger the sheet the harder it is to debug because one is not told clearly what the solver breaks on. In this scenario, having Undo would save a lot of time by pointing out where exactly the sheet broke.

Less common, but still important:
Less common, but still important:
*Comparing two different recipes for the same product*. In a production chain, one can compare two recipes for one product by adding both to the sheet and repeating Undo/Redo on the "enable recipe" setting for the one that is used when both are enabled. The alternative to this use case is to clone the sheet and do the setups in two separate sheets, but it takes more time and makes it harder to spot the differences.
13 changes: 10 additions & 3 deletions Yafc.Model/Blueprints/Blueprint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Yafc.Model;
using Yafc.UI;

namespace Yafc.Blueprints;

/// <summary>Specifies the serialisation format used when exporting a blueprint string.</summary>
public enum BlueprintFormat {
/// <summary>Produces a compressed, Base64-encoded Factorio blueprint string (the default game format).</summary>
CompressedBase64,
/// <summary>Produces raw JSON without compression, useful for debugging or tooling.</summary>
RawJson,
}

[Serializable]
public class BlueprintString(string blueprintName) {
public Blueprint blueprint { get; } = new Blueprint(blueprintName);
private static readonly byte[] header = [0x78, 0xDA];
private static readonly JsonSerializerOptions jsonSerializerOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };

public string ToBpString() {
if (InputSystem.Instance.control) {
public string ToBpString(BlueprintFormat format = BlueprintFormat.CompressedBase64) {
if (format == BlueprintFormat.RawJson) {
return ToJson();
}

Expand Down
23 changes: 17 additions & 6 deletions Yafc.Model/Blueprints/BlueprintUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
namespace Yafc.Blueprints;

public static class BlueprintUtilities {
public static string ExportConstantCombinators(string name, IReadOnlyList<(IObjectWithQuality<Goods> item, int amount)> goods) {
public static string ExportConstantCombinators(
string name,
IReadOnlyList<(IObjectWithQuality<Goods> item, int amount)> goods,
BlueprintFormat format = BlueprintFormat.CompressedBase64) {
int combinatorCount = ((goods.Count - 1) / Database.constantCombinatorCapacity) + 1;
int offset = -combinatorCount / 2;
BlueprintString blueprint = new BlueprintString(name);
Expand Down Expand Up @@ -37,10 +40,14 @@ public static string ExportConstantCombinators(string name, IReadOnlyList<(IObje
last = entity;
}

return blueprint.ToBpString();
return blueprint.ToBpString(format);
}

public static string ExportRequesterChests(string name, IReadOnlyList<(IObjectWithQuality<Item> item, int amount)> goods, EntityContainer chest) {
public static string ExportRequesterChests(
string name,
IReadOnlyList<(IObjectWithQuality<Item> item, int amount)> goods,
EntityContainer chest,
BlueprintFormat format = BlueprintFormat.CompressedBase64) {
if (chest.logisticSlotsCount <= 0) {
throw new ArgumentException("Chest does not have logistic slots");
}
Expand Down Expand Up @@ -73,7 +80,7 @@ public static string ExportRequesterChests(string name, IReadOnlyList<(IObjectWi
}
}

return blueprint.ToBpString();
return blueprint.ToBpString(format);
}

private class PlacedEntity {
Expand All @@ -94,7 +101,11 @@ private class LayoutResult {
public List<PlacedEntity> Placements { get; set; } = new();
}

public static string ExportRecipiesAsBlueprint(string name, IEnumerable<RecipeRow> recipies, bool includeFuel) {
public static string ExportRecipiesAsBlueprint(
string name,
IEnumerable<RecipeRow> recipies,
bool includeFuel,
BlueprintFormat format = BlueprintFormat.CompressedBase64) {
// Sort buildings largest to smallest (by height then width) for better packing
var entities = recipies
.Where(r => r.entity is not null && r.recipe is not null)
Expand Down Expand Up @@ -185,6 +196,6 @@ public static string ExportRecipiesAsBlueprint(string name, IEnumerable<RecipeRo
buildingIndex += 1;
}

return blueprint.ToBpString();
return blueprint.ToBpString(format);
}
}
20 changes: 20 additions & 0 deletions Yafc.Model/Serialization/IUndoBatchScheduler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading;

namespace Yafc.Model;

/// <summary>
/// Defines the scheduling contract for committing undo batches. The host application provides an
/// implementation that determines the appropriate moment to finalize a pending undo batch (e.g., when
/// the user releases the mouse after a drag gesture). A default <see cref="ImmediateUndoBatchScheduler"/>
/// is used in headless or test scenarios.
/// </summary>
public interface IUndoBatchScheduler {
/// <summary>
/// Schedules <paramref name="callback"/> to be invoked when the current interaction gesture is
/// complete. If no gesture is in progress the callback should be dispatched immediately or
/// asynchronously on the appropriate thread.
/// </summary>
/// <param name="callback">The callback to invoke once the gesture finishes.</param>
/// <param name="state">An opaque state object forwarded to <paramref name="callback"/>.</param>
void ScheduleOnGestureFinish(SendOrPostCallback callback, object state);
}
13 changes: 13 additions & 0 deletions Yafc.Model/Serialization/ImmediateUndoBatchScheduler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Threading;

namespace Yafc.Model;

/// <summary>
/// An <see cref="IUndoBatchScheduler"/> that invokes the callback synchronously and immediately,
/// without waiting for any interaction gesture to finish. This implementation is suitable for
/// headless execution, unit tests, and any context where no UI event loop is present.
/// </summary>
public sealed class ImmediateUndoBatchScheduler : IUndoBatchScheduler {
/// <inheritdoc />
public void ScheduleOnGestureFinish(SendOrPostCallback callback, object state) => callback(state);
}
21 changes: 19 additions & 2 deletions Yafc.Model/Serialization/UndoSystem.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using Yafc.UI;

namespace Yafc.Model;

public class UndoSystem {
/// <summary>
/// Gets or sets the fallback scheduler used when <see cref="UndoSystem()" /> is called without
/// an explicit scheduler argument. Set this to a UI-aware implementation (e.g.
/// <c>GestureFinishUndoBatchScheduler</c>) before creating any <see cref="Project"/> instances
/// in an interactive session. Defaults to <see cref="ImmediateUndoBatchScheduler"/>.
/// </summary>
public static IUndoBatchScheduler DefaultScheduler { get; set; } = new ImmediateUndoBatchScheduler();

private readonly IUndoBatchScheduler _scheduler;

/// <summary>Initialises a new <see cref="UndoSystem"/> using <see cref="DefaultScheduler"/>.</summary>
public UndoSystem() : this(DefaultScheduler) { }

/// <summary>Initialises a new <see cref="UndoSystem"/> with an explicit <paramref name="scheduler"/>.</summary>
public UndoSystem(IUndoBatchScheduler scheduler) {
_scheduler = scheduler;
}

public uint version { get; private set; } = 2;
private bool undoBatchVisualOnly = true;
private readonly List<UndoSnapshot> currentUndoBatch = [];
Expand Down Expand Up @@ -67,7 +84,7 @@ private static void MakeUndoBatch(object? state) {
}

private void Schedule() {
InputSystem.Instance.DispatchOnGestureFinish(MakeUndoBatch, this);
_scheduler.ScheduleOnGestureFinish(MakeUndoBatch, this);
scheduled = true;
}

Expand Down
16 changes: 16 additions & 0 deletions Yafc/GestureFinishUndoBatchScheduler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Threading;
using Yafc.Model;
using Yafc.UI;

namespace Yafc;

/// <summary>
/// An <see cref="IUndoBatchScheduler"/> that delegates to <see cref="InputSystem.DispatchOnGestureFinish"/>.
/// Undo batches are committed once the user releases the current mouse button (or immediately if no
/// mouse button is being held). This implementation is appropriate for interactive UI sessions.
/// </summary>
internal sealed class GestureFinishUndoBatchScheduler : IUndoBatchScheduler {
/// <inheritdoc />
public void ScheduleOnGestureFinish(SendOrPostCallback callback, object state)
=> InputSystem.Instance.DispatchOnGestureFinish(callback, state);
}
5 changes: 5 additions & 0 deletions Yafc/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public static class Program {
private static void Main(string[] args) {
YafcLib.RegisterDefaultAnalysis();

// Wire up the UI-aware undo batch scheduler so that undo batches are committed on gesture-finish
// (mouse-up) rather than immediately. This must be set before any Project is loaded or created,
// including any that Ui.Start() might trigger during initialization.
UndoSystem.DefaultScheduler = new GestureFinishUndoBatchScheduler();

try {
Ui.Start();
}
Expand Down
13 changes: 13 additions & 0 deletions Yafc/Utils/BlueprintClipboardContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Yafc.Blueprints;
using Yafc.UI;

namespace Yafc;

public static class BlueprintClipboardContext {
/// <summary>
/// Gets the blueprint format requested for clipboard export based on the current keyboard modifiers.
/// Hold Ctrl to export raw JSON; otherwise export compressed Base64.
/// </summary>
public static BlueprintFormat RequestedFormat
=> InputSystem.Instance.control ? BlueprintFormat.RawJson : BlueprintFormat.CompressedBase64;
}
12 changes: 9 additions & 3 deletions Yafc/Windows/ShoppingListScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,19 @@ private void ExportBlueprintDropdown(ImGui gui) {
gui.BuildText(LSs.ShoppingListExportBlueprintHint, TextBlockDisplayStyle.WrappedText);
if (Database.objectsByTypeName.TryGetValue("Entity.constant-combinator", out var combinator)
&& gui.BuildFactorioObjectButtonWithText(combinator) == Click.Left && gui.CloseDropdown()) {

_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportConstantCombinators(LSs.ShoppingList, ExportGoods<Goods>()));
_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportConstantCombinators(
LSs.ShoppingList,
ExportGoods<Goods>(),
format: BlueprintClipboardContext.RequestedFormat));
}

foreach (var container in Database.allContainers) {
if (container.logisticMode == "requester" && gui.BuildFactorioObjectButtonWithText(container) == Click.Left && gui.CloseDropdown()) {
_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportRequesterChests(LSs.ShoppingList, ExportGoods<Item>(), container));
_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportRequesterChests(
LSs.ShoppingList,
ExportGoods<Item>(),
container,
format: BlueprintClipboardContext.RequestedFormat));
}
}
}
Expand Down
15 changes: 11 additions & 4 deletions Yafc/Workspace/ProductionTable/ProductionTableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,10 @@ private void ExportIo(float multiplier) {

goods.Add((flow.goods, rounded));
}

_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportConstantCombinators(view.projectPage!.name, goods)); // null-forgiving: An active view always has an active page.
_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportConstantCombinators(
view.projectPage!.name, // null-forgiving: An active view always has an active page.
goods,
format: BlueprintClipboardContext.RequestedFormat));
}
}

Expand Down Expand Up @@ -597,7 +599,8 @@ private static void ShowEntityDropdown(ImGui gui, RecipeRow recipe) {
}

BlueprintString bp = new BlueprintString(recipe.recipe.target.locName) { blueprint = { entities = { entity } } };
_ = SDL.SDL_SetClipboardText(bp.ToBpString());
BlueprintFormat bpFormat = BlueprintClipboardContext.RequestedFormat;
_ = SDL.SDL_SetClipboardText(bp.ToBpString(bpFormat));
}
}

Expand Down Expand Up @@ -656,7 +659,11 @@ public override void BuildMenu(ImGui gui) {
.GetRecipesRecursive()
.DistinctBy(row => (row.entity, row.recipe, includeFuel ? row.fuel : null));

_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportRecipiesAsBlueprint(view.projectPage!.name, uniqueEntites, includeFuel));
_ = SDL.SDL_SetClipboardText(BlueprintUtilities.ExportRecipiesAsBlueprint(
view.projectPage!.name, // null-forgiving: An active view always has an active page.
uniqueEntites,
includeFuel,
format: BlueprintClipboardContext.RequestedFormat));
}
}
}
Expand Down
Loading