Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make milestone list scrollable and support more milestones #19

Merged
merged 3 commits into from
Feb 7, 2024
Merged
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
7 changes: 7 additions & 0 deletions FactorioCalc.sln
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YAFC", "YAFC\YAFC.csproj", "{73EBA162-A3BE-43CC-9B55-CA16332F439D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YAFCui", "YAFCui\YAFCui.csproj", "{70F74D2B-9747-4185-B369-64F77BC8D0D5}"
Expand All @@ -10,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YAFCparser", "YAFCparser\YA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineToolExample", "CommandLineToolExample\CommandLineToolExample.csproj", "{57E8CAE8-A3F8-4532-B32F-09347852479E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YAFCmodel.Tests", "YAFCmodel.Tests\YAFCmodel.Tests.csproj", "{66B66728-84F0-4242-B49A-B9D746A3CCA5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -36,5 +39,9 @@ Global
{57E8CAE8-A3F8-4532-B32F-09347852479E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57E8CAE8-A3F8-4532-B32F-09347852479E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57E8CAE8-A3F8-4532-B32F-09347852479E}.Release|Any CPU.Build.0 = Release|Any CPU
{66B66728-84F0-4242-B49A-B9D746A3CCA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{66B66728-84F0-4242-B49A-B9D746A3CCA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{66B66728-84F0-4242-B49A-B9D746A3CCA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{66B66728-84F0-4242-B49A-B9D746A3CCA5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
10 changes: 5 additions & 5 deletions YAFC/Widgets/ObjectTooltip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,18 @@ private void BuildHeader(ImGui gui)
if (extendHeader && !(target is Goods))
name = name +" (" + target.target.type + ")";
gui.BuildText(name, Font.header, true);
var milestoneMask = Milestones.Instance.milestoneResult[target.target];
if (milestoneMask > 1)
var milestoneMask = Milestones.Instance.GetMilestoneResult(target.target);
if (milestoneMask.HighestBitSet() > 0)
{
var spacing = MathF.Min(22f / Milestones.Instance.currentMilestones.Length - 1f, 0f);
using (gui.EnterRow(spacing))
{
var mask = 2ul;
var maskBit = 1;
foreach (var milestone in Milestones.Instance.currentMilestones)
{
if ((milestoneMask & mask) != 0)
if (milestoneMask[maskBit])
gui.BuildIcon(milestone.icon, 1f, SchemeColor.Source);
mask <<= 1;
maskBit++;
}
}
}
Expand Down
17 changes: 7 additions & 10 deletions YAFC/Windows/MilestonesEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ public override void Build(ImGui gui)
}
if (gui.BuildButton("Add milestone"))
{
if (Project.current.settings.milestones.Count >= 60)
MessageBox.Show(null, "Milestone limit reached", "60 milestones is the limit. You may delete some of the milestones you've already reached.", "Ok");
else SelectObjectPanel.Select(Database.objects.all, "Add new milestone", AddMilestone);
SelectObjectPanel.Select(Database.objects.all, "Add new milestone", AddMilestone);
}
}
}
Expand All @@ -74,25 +72,24 @@ private void AddMilestone(FactorioObject obj)
MessageBox.Show(null, "Milestone already exists", "Ok");
return;
}
var lockedMask = Milestones.Instance.milestoneResult[obj];
if (lockedMask == 0)
var lockedMask = Milestones.Instance.GetMilestoneResult(obj);
if (lockedMask.IsClear())
{
settings.RecordUndo().milestones.Add(obj);
}
else
{
var bestIndex = 0;
for (var i = 1; i < 64; i++)
for (var i = 1; i < lockedMask.length; i++)
{
var mask = (1ul << i);
if ((lockedMask & mask) != 0)
if (lockedMask[i])
{
lockedMask &= ~mask;
lockedMask[i] = false;
var milestone = Milestones.Instance.currentMilestones[i - 1];
var index = settings.milestones.IndexOf(milestone);
if (index >= bestIndex)
bestIndex = index + 1;
if (lockedMask == 0)
if (lockedMask.IsClear())
break;
}
}
Expand Down
58 changes: 30 additions & 28 deletions YAFC/Windows/MilestonesPanel.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
using System.Numerics;
using YAFC.Model;
using YAFC.UI;

namespace YAFC
{
public class MilestonesWidget
public class MilestonesWidget : VirtualScrollList<FactorioObject>
{
public static readonly MilestonesWidget Instance = new MilestonesWidget();
public void Build(ImGui gui)

public MilestonesWidget() : base(30f, new Vector2(3f, 3f), MilestoneDrawer)
{
data = Project.current.settings.milestones;
}

private static void MilestoneDrawer(ImGui gui, FactorioObject element, int index)
{
var settings = Project.current.settings;
using (var grid = gui.EnterInlineGrid(3f))
var unlocked = settings.Flags(element).HasFlags(ProjectPerItemFlags.MilestoneUnlocked);
if (gui.BuildFactorioObjectButton(element, 3f, display: MilestoneDisplay.None, bgColor: unlocked ? SchemeColor.Primary : SchemeColor.None))
{
foreach (var cur in settings.milestones)
if (!unlocked)
{
grid.Next();
var unlocked = settings.Flags(cur).HasFlags(ProjectPerItemFlags.MilestoneUnlocked);
if (gui.BuildFactorioObjectButton(cur, 3f, MilestoneDisplay.None, unlocked ? SchemeColor.Primary : SchemeColor.None))
var massUnlock = Milestones.Instance.GetMilestoneResult(element);
var subIndex = 0;
settings.SetFlag(element, ProjectPerItemFlags.MilestoneUnlocked, true);
foreach (var milestone in settings.milestones)
{
if (!unlocked)
{
var massUnlock = Milestones.Instance.milestoneResult[cur];
var subIndex = 0;
settings.SetFlag(cur, ProjectPerItemFlags.MilestoneUnlocked, true);
foreach (var milestone in settings.milestones)
{
subIndex++;
if ((massUnlock & (1ul << subIndex)) != 0)
settings.SetFlag(milestone, ProjectPerItemFlags.MilestoneUnlocked, true);
}
}
else
{
settings.SetFlag(cur, ProjectPerItemFlags.MilestoneUnlocked, false);
}
subIndex++;
if (massUnlock[subIndex])
settings.SetFlag(milestone, ProjectPerItemFlags.MilestoneUnlocked, true);
}
if (unlocked && gui.isBuilding)
gui.DrawIcon(gui.lastRect, Icon.Check, SchemeColor.Error);
}
else
{
settings.SetFlag(element, ProjectPerItemFlags.MilestoneUnlocked, false);
}
}
if (unlocked && gui.isBuilding)
gui.DrawIcon(gui.lastRect, Icon.Check, SchemeColor.Error);

}

}

public class MilestonesPanel : PseudoScreen
{
public static readonly MilestonesPanel Instance = new MilestonesPanel();
Expand All @@ -53,14 +55,14 @@ public override void Build(ImGui gui)
gui.AllocateSpacing(2f);
MilestonesWidget.Instance.Build(gui);
gui.AllocateSpacing(2f);
gui.BuildText("For your convinience, YAFC will show objects you DON'T have access to based on this selection", wrap:true);
gui.BuildText("For your convenience, YAFC will show objects you DON'T have access to based on this selection", wrap: true);
gui.BuildText("These are called 'Milestones'. By default all science packs are added as milestones, but this does not have to be this way! " +
"You can define your own milestones: Any item, recipe, entity or technology may be added as a milestone. For example you can add advanced " +
"electronic circuits as a milestone, and YAFC will display everything that is locked behind those circuits", wrap: true);
using (gui.EnterRow())
{
if (gui.BuildButton("Edit milestones", SchemeColor.Grey))
MilestonesEditor.Show();
MilestonesEditor.Show();
if (gui.RemainingRow().BuildButton("Done"))
Close();
}
Expand Down
133 changes: 133 additions & 0 deletions YAFCmodel.Tests/Analysis/Milestones.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Reflection;
using System.Collections.Generic;
using Xunit;

namespace YAFC.Model.Tests
{
public class MilestonesTests
{
private static Bits createBits(ulong value)
{
var bitsType = typeof(Bits);
var bitsData = bitsType.GetField("data", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var bitsLength = bitsType.GetField("_length", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

var bits = new Bits();
bitsData.SetValue(bits, new ulong[] { value });
bitsLength.SetValue(bits, sizeof(ulong));
bitsLength.SetValue(bits, bits.HighestBitSet() + 1);

return bits;
}
private static Milestones setupMilestones(ulong result, ulong mask, out FactorioObject factorioObj)
{
factorioObj = new Technology();
var milestoneResult = new Mapping<FactorioObject, Bits>(new FactorioIdRange<FactorioObject>(0, 1, new List<FactorioObject>() {
factorioObj
}));
milestoneResult[factorioObj] = createBits(result);


var milestonesType = typeof(Milestones);
var milestonesLockedMask = milestonesType.GetProperty("lockedMask");
var milestoneResultField = milestonesType.GetField("milestoneResult", BindingFlags.NonPublic | BindingFlags.Instance);

var milestones = new Milestones()
{
currentMilestones = new FactorioObject[] { factorioObj }
};

milestoneResultField.SetValue(milestones, milestoneResult);
milestonesLockedMask.SetValue(milestones, createBits(mask));

return milestones;
}

[Theory]
[InlineData(0, 1, false)]
[InlineData(1, 0, false)]
[InlineData(1, 1, true)]
[InlineData(3, 1, true)]
[InlineData(1, 3, true)]
public void IsAccessibleWithCurrentMilestones_WhenGivenMilestones_ShouldReturnCorrectValue(ulong result, ulong mask, bool expectedResult)
{
var milestones = setupMilestones(result, mask, out FactorioObject factorioObj);

Assert.Equal(expectedResult, milestones.IsAccessibleWithCurrentMilestones(0));
Assert.Equal(expectedResult, milestones.IsAccessibleWithCurrentMilestones(factorioObj));
}

[Theory]
[InlineData(1, 1, true)]
[InlineData(3, 3, false)]
[InlineData(15, 15, false)] // Triggers last return
[InlineData(16, 16, false)] // Triggers last return
[InlineData(17, 17, false)] // Caught by 'bit 0 check', otherwise last return would return true
public void IsAccessibleAtNextMilestone_WhenGivenMilestones_ShouldReturnCorrectValue(ulong result, ulong mask, bool expectedResult)
{
var milestones = setupMilestones(result, mask, out FactorioObject factorioObj);

Assert.Equal(expectedResult, milestones.IsAccessibleAtNextMilestone(factorioObj));
}

[Theory]
[InlineData(false, new int[] { })] // all bits set (nothing gets masked)
[InlineData(true, new int[] { 1 })] // all bits set, except bit 1 (for reasons not bit 0, even if the FIRST milestone has its flag set?!)
public void GetLockedMaskFromProject_ShouldCalculateMask(bool unlocked, int[] bitsCleared)
{
var milestonesType = typeof(Milestones);
var getLockedMaskFromProject = milestonesType.GetMethod("GetLockedMaskFromProject", BindingFlags.NonPublic | BindingFlags.Instance);
var projectField = milestonesType.GetField("project", BindingFlags.NonPublic | BindingFlags.Instance);

var milestones = setupMilestones(0, 0, out FactorioObject factorioObj);

var project = new Project();
if (unlocked)
{
// Can't use SetFlag() as it uses the Undo system, which requires SDL
var flags = project.settings.itemFlags;
flags[factorioObj] = ProjectPerItemFlags.MilestoneUnlocked;
var projectType = typeof(ProjectSettings);
var itemFlagsField = projectType.GetField("<itemFlags>k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance);
itemFlagsField.SetValue(project.settings, flags);
}

projectField.SetValue(milestones, project);

getLockedMaskFromProject.Invoke(milestones, null);
var lockedBits = milestones.lockedMask;

var index = 0;
for (int i = 0; i < lockedBits.length; i++)
{
var expectSet = index == bitsCleared.Length || bitsCleared[index] != i;
Assert.True(expectSet == lockedBits[i], "bit " + i + " is expected to be " + (expectSet ? "set" : "cleared"));
if (index < bitsCleared.Length && bitsCleared[index] == i)
{
index++;
}
}
}

[Theory]
[InlineData(1, 0, true, false)] // HighestBitSet() - 1, so bit 0 is never in range...
[InlineData(2, 0, true, true)] // mask is ignored -> true
[InlineData(2, 0, false, false)] // mask is active -> false
[InlineData(2, 2, true, true)]
[InlineData(2, 2, false, true)] // mask is active and overlaps -> true
[InlineData(4, 0, true, false)] // HighestBitSet() too large...
public void GetHighest_WhenGivenMilestones_ShouldReturnCorrectValue(ulong result, ulong mask, bool all, bool expectObject)
{
var milestones = setupMilestones(result, mask, out FactorioObject factorioObj);

if (expectObject)
{
Assert.Equal(factorioObj, milestones.GetHighest(factorioObj, all));
}
else
{
Assert.Null(milestones.GetHighest(factorioObj, all));
}
}
}
}
Loading