Skip to content

Commit

Permalink
Merge branch 'develop' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
Pathoschild committed May 25, 2024
2 parents 55aee0f + 782a3a2 commit f664ecc
Show file tree
Hide file tree
Showing 28 changed files with 291 additions and 1,235 deletions.
2 changes: 1 addition & 1 deletion Automate/Automate.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>2.1.0</Version>
<Version>2.2.0</Version>
<RootNamespace>Pathoschild.Stardew.Automate</RootNamespace>

<TranslationClassBuilder_AddGetByKey>true</TranslationClassBuilder_AddGetByKey>
Expand Down
11 changes: 8 additions & 3 deletions Automate/Framework/AutomationFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal class AutomationFactory : IAutomationFactory
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;

/// <summary>Simplifies access to private code.</summary>
private readonly IReflectionHelper Reflection;

/// <summary>Whether the Better Junimos mod is installed.</summary>
private readonly bool IsBetterJunimosLoaded;

Expand All @@ -39,11 +42,13 @@ internal class AutomationFactory : IAutomationFactory
/// <summary>Construct an instance.</summary>
/// <param name="config">The mod configuration.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="isBetterJunimosLoaded">Whether the Better Junimos mod is installed.</param>
public AutomationFactory(Func<ModConfig> config, IMonitor monitor, bool isBetterJunimosLoaded)
public AutomationFactory(Func<ModConfig> config, IMonitor monitor, IReflectionHelper reflection, bool isBetterJunimosLoaded)
{
this.Config = config;
this.Monitor = monitor;
this.Reflection = reflection;
this.IsBetterJunimosLoaded = isBetterJunimosLoaded;
}

Expand Down Expand Up @@ -118,8 +123,8 @@ public AutomationFactory(Func<ModConfig> config, IMonitor monitor, bool isBetter
case FruitTree fruitTree:
return new FruitTreeMachine(fruitTree, location, tile);

case Tree tree when TreeMachine.CanAutomate(tree, location) && tree.growthStage.Value >= Tree.treeStage: // avoid accidental machine links due to seeds spreading automatically
return new TreeMachine(tree, location, tile, this.Config().CollectTreeMoss);
case Tree tree when TreeMachine.CanAutomate(tree) && tree.growthStage.Value >= Tree.treeStage: // avoid accidental machine links due to seeds spreading automatically
return new TreeMachine(tree, location, tile, this.Config().CollectTreeMoss, this.Reflection);
}

// connector
Expand Down
19 changes: 9 additions & 10 deletions Automate/Framework/Machines/Objects/FeedHopperMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ public FeedHopperMachine(Building silo, GameLocation location)
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
Farm farm = Game1.getFarm();
return this.GetFreeSpace(farm) > 0
return this.GetFreeSpace(this.Location) > 0
? MachineState.Empty // 'empty' insofar as it will accept more input, not necessarily empty
: MachineState.Disabled;
}
Expand All @@ -46,24 +45,24 @@ public override MachineState GetState()
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
Farm farm = Game1.getFarm();
GameLocation location = this.Location;

// skip if full
if (this.GetFreeSpace(farm) <= 0)
if (this.GetFreeSpace(location) <= 0)
return false;

// try to add hay (178) until full
bool anyPulled = false;
foreach (ITrackedStack stack in input.GetItems().Where(p => p.Sample.QualifiedItemId == "(O)178"))
{
// get free space
int space = this.GetFreeSpace(farm);
int space = this.GetFreeSpace(location);
if (space <= 0)
return anyPulled;

// pull hay
int maxToAdd = Math.Min(stack.Count, space);
int added = maxToAdd - farm.tryToAddHay(maxToAdd);
int added = maxToAdd - location.tryToAddHay(maxToAdd);
stack.Reduce(added);
if (added > 0)
anyPulled = true;
Expand All @@ -77,11 +76,11 @@ public override bool SetInput(IStorage input)
** Private methods
*********/
/// <summary>Get the amount of hay the hopper can still accept before it's full.</summary>
/// <param name="farm">The farm to check.</param>
/// <remarks>Derived from <see cref="Farm.tryToAddHay"/>.</remarks>
private int GetFreeSpace(Farm farm)
/// <param name="location">The location to check.</param>
/// <remarks>Derived from <see cref="GameLocation.tryToAddHay"/>.</remarks>
private int GetFreeSpace(GameLocation location)
{
return farm.GetHayCapacity() - farm.piecesOfHay.Value;
return location.GetHayCapacity() - location.piecesOfHay.Value;
}
}
}
195 changes: 123 additions & 72 deletions Automate/Framework/Machines/TerrainFeatures/TreeMachine.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Common.Utilities;
using StardewModdingAPI;
using StardewValley;
using StardewValley.GameData.WildTrees;
using StardewValley.Locations;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;

namespace Pathoschild.Stardew.Automate.Framework.Machines.TerrainFeatures
{
Expand All @@ -17,11 +16,17 @@ internal class TreeMachine : BaseMachine<Tree>
/*********
** Fields
*********/
/// <summary>Get a dropped item if its fields match.</summary>
private readonly IReflectedMethod TryGetDrop;

/// <summary>Whether to collect moss on the tree.</summary>
private readonly bool CollectMoss;

/// <summary>The items to drop.</summary>
private readonly Cached<Stack<string>> ItemDrops;
/// <summary>The moss item to drop.</summary>
private readonly Cached<Item?> MossDrop;

/// <summary>The seed items to drop.</summary>
private readonly Cached<Stack<Item>> SeedDrops;


/*********
Expand All @@ -32,14 +37,24 @@ internal class TreeMachine : BaseMachine<Tree>
/// <param name="location">The machine's in-game location.</param>
/// <param name="tile">The tree's tile position.</param>
/// <param name="collectMoss">Whether to collect moss on the tree.</param>
public TreeMachine(Tree tree, GameLocation location, Vector2 tile, bool collectMoss)
/// <param name="reflection">Simplifies access to private code.</param>
public TreeMachine(Tree tree, GameLocation location, Vector2 tile, bool collectMoss, IReflectionHelper reflection)
: base(tree, location, BaseMachine.GetTileAreaFor(tile))
{
this.CollectMoss = collectMoss;

this.ItemDrops = new Cached<Stack<string>>(
getCacheKey: () => $"{Game1.season},{Game1.dayOfMonth},{tree.hasSeed.Value},{this.CollectMoss && tree.hasMoss.Value}",
fetchNew: () => new Stack<string>()
this.TryGetDrop = reflection.GetMethod(tree, "TryGetDrop");

// create cached output fields
// These are needed because the tree can drop multiple items at once, but the chest may only have space for
// some of them. Since the items are normally just dropped on the ground, trees have no way to track
// partial collection. These cache fields let us fetch the drops once and then output them incrementally.
this.MossDrop = new Cached<Item?>(
getCacheKey: () => $"{Game1.season},{Game1.dayOfMonth},{tree.hasMoss.Value}",
fetchNew: this.InitMossOutput
);
this.SeedDrops = new Cached<Stack<Item>>(
getCacheKey: () => $"{Game1.season},{Game1.dayOfMonth},{tree.hasSeed.Value}",
fetchNew: this.InitSeedOutput
);
}

Expand All @@ -49,7 +64,7 @@ public override MachineState GetState()
if (this.Machine.growthStage.Value < Tree.treeStage || this.Machine.stump.Value)
return MachineState.Disabled;

return this.HasSeed() || this.CanCollectMoss()
return this.CanCollectMoss() || this.CanCollectSeeds()
? MachineState.Done
: MachineState.Processing;
}
Expand All @@ -59,32 +74,32 @@ public override MachineState GetState()
{
Tree tree = this.Machine;

// recalculate drop queue
var drops = this.ItemDrops.Value;
if (!drops.Any())
// moss
if (this.CanCollectMoss())
{
// seed must be last item dropped, since collecting the seed will reset the stack
string? seedId = TreeMachine.GetSeedForTree(tree, this.Location);
if (seedId != null)
drops.Push(seedId);

// get extra drops
foreach (string itemId in this.GetRandomExtraDrops())
drops.Push(itemId);
Item? mossDrop = this.MossDrop.Value;
if (mossDrop is not null)
return new TrackedItem(mossDrop, onEmpty: _ => tree.hasMoss.Value = false);
}

// get moss
if (this.CanCollectMoss())
// seeds
if (this.CanCollectSeeds())
{
Stack<Item> seedDrops = this.SeedDrops.Value;
if (seedDrops.TryPeek(out Item? nextSeed))
{
Item item = Tree.CreateMossItem();
for (int i = 0; i < item.Stack; i++)
drops.Push(item.ItemId);
return new TrackedItem(nextSeed, onEmpty: _ =>
{
if (seedDrops.Count > 0)
seedDrops.Pop();
if (seedDrops.Count == 0)
tree.hasSeed.Value = false;
});
}
}

// get next drop
return drops.Any()
? new TrackedItem(ItemRegistry.Create(drops.Peek()), onReduced: this.OnOutputReduced)
: null;
return null;
}

/// <summary>Provide input to the machine.</summary>
Expand All @@ -97,73 +112,109 @@ public override bool SetInput(IStorage input)

/// <summary>Get whether a tree can be automated.</summary>
/// <param name="tree">The tree to automate.</param>
/// <param name="location">The location containing the tree.</param>
public static bool CanAutomate(Tree tree, GameLocation location)
public static bool CanAutomate(Tree tree)
{
return TreeMachine.GetSeedForTree(tree, location) != null;
WildTreeData? data = tree.GetData();
if (data is null)
return false;

return
data.GrowsMoss
|| (
data.SeedOnShakeChance > 0
&& (data.SeedItemId != null || data.SeedDropItems?.Count > 0)
);
}


/*********
** Private methods
*********/
/// <summary>Reset the machine so it's ready to accept a new input.</summary>
/// <param name="item">The output item that was taken.</param>
private void OnOutputReduced(Item item)
/// <summary>Get whether the tree has moss that can be collected.</summary>
private bool CanCollectMoss()
{
Tree tree = this.Machine;

if (ItemRegistry.HasItemId(item, TreeMachine.GetSeedForTree(tree, this.Location)))
tree.hasSeed.Value = false;
if (ItemRegistry.HasItemId(item, "(O)Moss"))
tree.hasMoss.Value = false;

Stack<string> drops = this.ItemDrops.Value;
if (drops.Any() && drops.Peek() == item.ItemId)
drops.Pop();
return this.CollectMoss && this.Machine.hasMoss.Value;
}

/// <summary>Get whether the tree has a seed to drop.</summary>
private bool HasSeed()
/// <summary>Get whether the tree has seeds that can be collected.</summary>
private bool CanCollectSeeds()
{
return
this.Machine.hasSeed.Value
&& (Game1.IsMultiplayer || Game1.player.ForagingLevel >= 1);
}

/// <summary>Get whether the tree has grown moss</summary>>
private bool CanCollectMoss()
/// <summary>Get the moss to drop for the current tree.</summary>
/// <remarks>Derived from <see cref="Tree.shake"/>.</remarks>
private Item? InitMossOutput()
{
return this.CollectMoss && this.Machine.hasMoss.Value;
return this.CanCollectMoss()
? Tree.CreateMossItem()
: null;
}

/// <summary>Get the random items that should also drop when this tree has a seed.</summary>
private IEnumerable<string> GetRandomExtraDrops()
/// <summary>Get the seeds to drop for the current tree.</summary>
/// <remarks>Derived from <see cref="Tree.shake"/>.</remarks>
private Stack<Item> InitSeedOutput(Stack<Item>? stack)
{
Tree tree = this.Machine;
string type = tree.treeType.Value;
stack ??= new Stack<Item>();
stack.Clear();

// golden coconut
if (type is (Tree.palmTree or Tree.palmTree2) && this.Location is IslandLocation && new Random((int)Game1.uniqueIDForThisGame + (int)Game1.stats.DaysPlayed + this.TileArea.X * 13 + this.TileArea.Y * 54).NextDouble() < 0.1)
yield return "791";
if (this.CanCollectSeeds())
{
WildTreeData? data = this.Machine.GetData();

// Qi bean
if (Game1.random.NextDouble() <= 0.5 && Game1.player.team.SpecialOrderRuleActive("DROP_QI_BEANS"))
yield return "890";
if (data is not null)
{
bool dropDefaultSeed = true;

// add SeedDropItems drops
if (data.SeedDropItems?.Count > 0)
{
foreach (WildTreeSeedDropItemData? drop in data.SeedDropItems)
{
if (drop is null)
continue;

Item? seed = this.TryGetDrop.Invoke<Item?>(drop, Game1.random, Game1.player,
nameof(data.SeedDropItems));
if (seed is null)
continue;

stack.Push(this.PrepareSeedItem(seed));

if (!drop.ContinueOnDrop)
{
dropDefaultSeed = false;
break;
}
}
}

// drop default seed
if (dropDefaultSeed && data.SeedItemId is not null)
{
Item seed = this.PrepareSeedItem(
ItemRegistry.Create(data.SeedItemId)
);
stack.Push(seed);
}

// random Qi bean drop if tree has a seed
if (stack.Count > 0 && Game1.random.NextDouble() <= 0.5 && Game1.player.team.SpecialOrderRuleActive("DROP_QI_BEANS"))
stack.Push(ItemRegistry.Create("(O)890"));
}
}

return stack;
}

/// <summary>Get the seed ID dropped by a tree, regardless of whether it currently has a seed.</summary>
/// <param name="tree">The tree instance.</param>
/// <param name="location">The location containing the tree.</param>
/// <remarks>Derived from <see cref="Tree.shake"/>.</remarks>
private static string? GetSeedForTree(Tree tree, GameLocation location)
/// <summary>Apply profession bonuses and tracking data to a seed item before it's output.</summary>
/// <param name="seed">The item that was produced.</param>
private Item PrepareSeedItem(Item seed)
{
WildTreeData? data = tree.GetData();

string? seed = data?.SeedItemId;
if (Game1.GetSeasonForLocation(location) == Season.Fall && seed == "309"/*acorn*/ && Game1.dayOfMonth >= 14)
seed = "408";/*hazelnut*/
if (Game1.player.professions.Contains(Farmer.botanist) && seed.HasContextTag("forage_item"))
seed.Quality = SObject.bestQuality;

return seed;
}
Expand Down
1 change: 1 addition & 0 deletions Automate/ModEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public override void Entry(IModHelper helper)
defaultFactory: new AutomationFactory(
config: () => this.Config,
monitor: this.Monitor,
reflection: this.Helper.Reflection,
isBetterJunimosLoaded: helper.ModRegistry.IsLoaded("hawkfalcon.BetterJunimos")
),
monitor: this.Monitor
Expand Down
Loading

0 comments on commit f664ecc

Please sign in to comment.