Android Mod Transportation

This commit is contained in:
yangzhi 2019-04-10 01:22:10 +08:00
parent 9e521091fe
commit 44ddc4ca42
191 changed files with 13024 additions and 0 deletions

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8B08A816-6125-4277-A9EE-CA6AF9E279FC}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>AutoFish</RootNamespace>
<AssemblyName>AutoFish</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mod">
<HintPath>..\assemblies\Mod.dll</HintPath>
</Reference>
<Reference Include="StardewValley">
<HintPath>..\assemblies\StardewValley.dll</HintPath>
</Reference>
<Reference Include="BmFont, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\BmFont.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.Downloader, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.Downloader.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.ZipFile, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Licensing, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Licensing.dll</HintPath>
</Reference>
<Reference Include="Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Java.Interop.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Android.dll</HintPath>
</Reference>
<Reference Include="Mono.Security, Version=2.0.5.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Security.dll</HintPath>
</Reference>
<Reference Include="MonoGame.Framework, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\MonoGame.Framework.dll</HintPath>
</Reference>
<Reference Include="mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\mscorlib.dll</HintPath>
</Reference>
<Reference Include="System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.dll</HintPath>
</Reference>
<Reference Include="System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Xml</HintPath>
</Reference>
<Reference Include="System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\assemblies\System.Net.Http</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Runtime.Serialization</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Core.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Core.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Runtime, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Annotations, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Annotations.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.UI, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.UI.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.Utils, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.Utils.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Fragment, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Fragment.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Media.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Media.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.v4, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.v4.dll</HintPath>
</Reference>
<Reference Include="xTile, Version=1.0.7033.16602, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\xTile.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AutoFish\ModConfig.cs" />
<Compile Include="AutoFish\ModEntry.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoFish
{
class ModConfig
{
public bool maxCastPower { get; set; } = true;
public bool autoHit { get; set; } = true;
public bool fastBite { get; set; } = false;
public bool catchTreasure { get; set; } = true;
}
}

View File

@ -0,0 +1,138 @@
using SMDroid.Options;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using StardewValley.Menus;
using StardewValley.Tools;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoFish
{
public class ModEntry : Mod
{
private ModConfig Config;
private bool catching = false;
public override void Entry(IModHelper helper)
{
this.Config = this.Helper.ReadConfig<ModConfig>();
helper.Events.GameLoop.UpdateTicked += this.UpdateTick;
}
public override List<OptionsElement> GetConfigMenuItems()
{
List<OptionsElement> options = new List<OptionsElement>();
ModOptionsCheckbox _optionsCheckboxAutoHit = new ModOptionsCheckbox("自动起钩", 0x8765, delegate (bool value) {
this.Config.autoHit = value;
this.Helper.WriteConfig<ModConfig>(this.Config);
}, -1, -1);
_optionsCheckboxAutoHit.isChecked = this.Config.autoHit;
options.Add(_optionsCheckboxAutoHit);
ModOptionsCheckbox _optionsCheckboxMaxCastPower = new ModOptionsCheckbox("最大抛竿", 0x8765, delegate (bool value) {
this.Config.maxCastPower = value;
this.Helper.WriteConfig<ModConfig>(this.Config);
}, -1, -1);
_optionsCheckboxMaxCastPower.isChecked = this.Config.maxCastPower;
options.Add(_optionsCheckboxMaxCastPower);
ModOptionsCheckbox _optionsCheckboxFastBite = new ModOptionsCheckbox("快速咬钩", 0x8765, delegate (bool value) {
this.Config.fastBite = value;
this.Helper.WriteConfig<ModConfig>(this.Config);
}, -1, -1);
_optionsCheckboxFastBite.isChecked = this.Config.fastBite;
options.Add(_optionsCheckboxFastBite);
ModOptionsCheckbox _optionsCheckboxCatchTreasure = new ModOptionsCheckbox("钓取宝箱", 0x8765, delegate (bool value) {
this.Config.catchTreasure = value;
this.Helper.WriteConfig<ModConfig>(this.Config);
}, -1, -1);
_optionsCheckboxCatchTreasure.isChecked = this.Config.catchTreasure;
options.Add(_optionsCheckboxCatchTreasure);
return options;
}
private void UpdateTick(object sender, EventArgs e)
{
if (Game1.player == null)
return;
if (Game1.player.CurrentTool is FishingRod)
{
FishingRod currentTool = Game1.player.CurrentTool as FishingRod;
if (this.Config.fastBite && currentTool.timeUntilFishingBite > 0)
currentTool.timeUntilFishingBite /= 2; // 快速咬钩
if (this.Config.autoHit && currentTool.isNibbling && !currentTool.isReeling && !currentTool.hit && !currentTool.pullingOutOfWater && !currentTool.fishCaught)
currentTool.DoFunction(Game1.player.currentLocation, 1, 1, 1, Game1.player); // 自动咬钩
if (this.Config.maxCastPower)
currentTool.castingPower = 1;
}
if (Game1.activeClickableMenu is BobberBar) // 自动小游戏
{
BobberBar bar = Game1.activeClickableMenu as BobberBar;
float barPos = this.Helper.Reflection.GetField<float>(bar, "bobberBarPos").GetValue();
float barHeight = this.Helper.Reflection.GetField<int>(bar, "bobberBarHeight").GetValue();
float fishPos = this.Helper.Reflection.GetField<float>(bar, "bobberPosition").GetValue();
float treasurePos = this.Helper.Reflection.GetField<float>(bar, "treasurePosition").GetValue();
float distanceFromCatching = this.Helper.Reflection.GetField<float>(bar, "distanceFromCatching").GetValue();
bool treasureCaught = this.Helper.Reflection.GetField<bool>(bar, "treasureCaught").GetValue();
bool hasTreasure = this.Helper.Reflection.GetField<bool>(bar, "treasure").GetValue();
float treasureScale = this.Helper.Reflection.GetField<float>(bar, "treasureScale").GetValue();
float bobberBarSpeed = this.Helper.Reflection.GetField<float>(bar, "bobberBarSpeed").GetValue();
float barPosMax = 568 - barHeight;
float min = barPos + barHeight / 4,
max = barPos + barHeight / 1.5f;
if (this.Config.catchTreasure && hasTreasure && !treasureCaught && (distanceFromCatching > 0.75 || this.catching))
{
this.catching = true;
fishPos = treasurePos;
}
if (this.catching && distanceFromCatching < 0.15)
{
this.catching = false;
fishPos = this.Helper.Reflection.GetField<float>(bar, "bobberPosition").GetValue();
}
if (fishPos < min)
{
bobberBarSpeed -= 0.35f + (min - fishPos) / 20;
this.Helper.Reflection.GetField<float>(bar, "bobberBarSpeed").SetValue(bobberBarSpeed);
} else if (fishPos > max)
{
bobberBarSpeed += 0.35f + (fishPos - max) / 20;
this.Helper.Reflection.GetField<float>(bar, "bobberBarSpeed").SetValue(bobberBarSpeed);
} else
{
float target = 0.1f;
if (bobberBarSpeed > target)
{
bobberBarSpeed -= 0.1f + (bobberBarSpeed - target) / 25;
if (barPos + bobberBarSpeed > barPosMax)
bobberBarSpeed /= 2; // 减小触底反弹
if (bobberBarSpeed < target)
bobberBarSpeed = target;
} else
{
bobberBarSpeed += 0.1f + (target - bobberBarSpeed) / 25;
if (barPos + bobberBarSpeed < 0)
bobberBarSpeed /= 2; // 减小触顶反弹
if (bobberBarSpeed > target)
bobberBarSpeed = target;
}
this.Helper.Reflection.GetField<float>(bar, "bobberBarSpeed").SetValue(bobberBarSpeed);
}
}
else
{
this.catching = false;
}
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("AutoFish")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AutoFish")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("8b08a816-6125-4277-a9ee-ca6af9e279fc")]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5B089EEE-F22C-4753-B90D-16D4CD3F5D61}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>AutoSpeed</RootNamespace>
<AssemblyName>AutoSpeed</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mod">
<HintPath>..\assemblies\Mod.dll</HintPath>
</Reference>
<Reference Include="StardewValley">
<HintPath>..\assemblies\StardewValley.dll</HintPath>
</Reference>
<Reference Include="BmFont, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\BmFont.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.Downloader, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.Downloader.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.ZipFile, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Licensing, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Licensing.dll</HintPath>
</Reference>
<Reference Include="Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Java.Interop.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Android.dll</HintPath>
</Reference>
<Reference Include="Mono.Security, Version=2.0.5.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Security.dll</HintPath>
</Reference>
<Reference Include="MonoGame.Framework, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\MonoGame.Framework.dll</HintPath>
</Reference>
<Reference Include="mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\mscorlib.dll</HintPath>
</Reference>
<Reference Include="System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.dll</HintPath>
</Reference>
<Reference Include="System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Xml</HintPath>
</Reference>
<Reference Include="System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\assemblies\System.Net.Http</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Runtime.Serialization</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Core.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Core.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Runtime, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Annotations, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Annotations.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.UI, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.UI.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.Utils, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.Utils.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Fragment, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Fragment.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Media.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Media.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.v4, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.v4.dll</HintPath>
</Reference>
<Reference Include="xTile, Version=1.0.7033.16602, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\xTile.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="AutoSpeed\AutoSpeed.cs" />
<Compile Include="AutoSpeed\Framework\ModConfig.cs" />
<Compile Include="AutoSpeed\obj\Debug\TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs" />
<Compile Include="AutoSpeed\obj\Debug\TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs" />
<Compile Include="AutoSpeed\obj\Debug\TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="AutoSpeed\manifest.json" />
<None Include="AutoSpeed\obj\Debug\AutoSpeed.csproj.CoreCompileInputs.cache" />
<None Include="AutoSpeed\obj\Debug\AutoSpeed.csprojAssemblyReference.cache" />
<None Include="AutoSpeed\README.md" />
</ItemGroup>
<ItemGroup>
<Folder Include="AutoSpeed\bin\Debug\" />
<Folder Include="AutoSpeed\obj\Debug\TempPE\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,58 @@
using System.Collections.Generic;
using Omegasis.AutoSpeed.Framework;
using SMDroid.Options;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using StardewValley.Menus;
namespace Omegasis.AutoSpeed
{
/// <summary>The mod entry point.</summary>
public class AutoSpeed : Mod
{
/*********
** Fields
*********/
/// <summary>The mod configuration.</summary>
private ModConfig Config;
/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
this.Config = helper.ReadConfig<ModConfig>();
}
public override List<OptionsElement> GetConfigMenuItems()
{
List<OptionsElement> options = new List<OptionsElement>();
ModOptionsSlider _optionsSliderSpeed = new ModOptionsSlider("移动加速", 0x8765, delegate (int value) {
this.Config.Speed = value;
this.Helper.WriteConfig<ModConfig>(this.Config);
}, -1, -1);
_optionsSliderSpeed.sliderMinValue = 0;
_optionsSliderSpeed.sliderMaxValue = 10;
_optionsSliderSpeed.value = this.Config.Speed;
options.Add(_optionsSliderSpeed);
return options;
}
/*********
** Private methods
*********/
/// <summary>Raised after the game state is updated (≈60 times per second).</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
{
if (Context.IsPlayerFree)
Game1.player.addedSpeed = this.Config.Speed;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Omegasis.AutoSpeed.Framework
{
/// <summary>The mod configuration.</summary>
internal class ModConfig
{
/// <summary>The added speed.</summary>
public int Speed { get; set; } = 5;
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AutoSpeed
{
class ModConfig
{
/// <summary>The added speed.</summary>
public int Speed { get; set; } = 5;
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using StardewValley.Menus;
using SMDroid.Options;
namespace AutoSpeed
{
/// <summary>The mod entry point.</summary>
class ModEntry : StardewModdingAPI.Mod
{
/// <summary>The mod configuration.</summary>
private ModConfig Config;
/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
this.Config = helper.ReadConfig<ModConfig>();
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
}
public override List<OptionsElement> GetConfigMenuItems()
{
List<OptionsElement> options = new List<OptionsElement>();
ModOptionsSlider _optionsSliderSpeed = new ModOptionsSlider("移动加速", 0x8765, delegate (int value) {
Config.Speed = value;
Helper.WriteConfig<ModConfig>(Config);
}, -1, -1);
_optionsSliderSpeed.sliderMinValue = 0;
_optionsSliderSpeed.sliderMaxValue = 10;
_optionsSliderSpeed.value = Config.Speed;
options.Add(_optionsSliderSpeed);
return options;
}
/*********
** Private methods
*********/
/// <summary>Raised after the game state is updated (≈60 times per second).</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
{
if (Context.IsPlayerFree)
Game1.player.addedSpeed = Config.Speed;
}
}
}

View File

@ -0,0 +1,31 @@
**Auto Speed** is a [Stardew Valley](http://stardewvalley.net/) mod which lets you move faster
without the need to enter commands in the console.
Compatible with Stardew Valley 1.2+ on Linux, Mac, and Windows.
## Installation
1. [Install the latest version of SMAPI](https://github.com/Pathoschild/SMAPI/releases).
2. Install [this mod from Nexus mods](http://www.nexusmods.com/stardewvalley/mods/443).
3. Run the game using SMAPI.
## Usage
Launch the game with the mod installed to generate the config file, then edit the `config.json` to
set the speed you want (higher values are faster).
## Versions
1.0
* Initial release.
1.1:
* Updated to Stardew Valley 1.1 and SMAPI 0.40 1.1-3.
1.3:
* Updated to Stardew Valley 1.2 and SMAPI 1.12.
1.4:
* Switched to standard JSON config file.
* Fixed config defaulting to normal speed.
* Internal refactoring.
1.4.1:
* Enabled update checks in SMAPI 2.0+.

View File

@ -0,0 +1,10 @@
{
"Name": "Auto Speed",
"Author": "Alpha_Omegasis",
"Version": "1.8.0",
"Description": "Got to go fast!",
"UniqueID": "Omegasis.AutoSpeed",
"EntryDll": "AutoSpeed.dll",
"MinimumApiVersion": "2.10.1",
"UpdateKeys": [ "Nexus:443" ]
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("AutoSpeed")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AutoSpeed")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
[assembly: ComVisible(false)]
// 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
[assembly: Guid("5b089eee-f22c-4753-b90d-16d4cd3f5d61")]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
//通过使用 "*",如下所示:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,277 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5EF944E3-D54B-4936-B507-A40C17B17B8E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Automate</RootNamespace>
<AssemblyName>Automate</AssemblyName>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mod">
<HintPath>..\assemblies\Mod.dll</HintPath>
</Reference>
<Reference Include="StardewValley">
<HintPath>..\assemblies\StardewValley.dll</HintPath>
</Reference>
<Reference Include="BmFont, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\BmFont.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.Downloader, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.Downloader.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Expansion.ZipFile, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Expansion.ZipFile.dll</HintPath>
</Reference>
<Reference Include="Google.Android.Vending.Licensing, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Google.Android.Vending.Licensing.dll</HintPath>
</Reference>
<Reference Include="Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Java.Interop.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Analytics.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Analytics.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.dll</HintPath>
</Reference>
<Reference Include="Microsoft.AppCenter.Crashes.Android.Bindings, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Microsoft.AppCenter.Crashes.Android.Bindings.dll</HintPath>
</Reference>
<Reference Include="Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=84e04ff9cfb79065, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Android.dll</HintPath>
</Reference>
<Reference Include="Mono.Security, Version=2.0.5.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Mono.Security.dll</HintPath>
</Reference>
<Reference Include="MonoGame.Framework, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\MonoGame.Framework.dll</HintPath>
</Reference>
<Reference Include="mscorlib, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\mscorlib.dll</HintPath>
</Reference>
<Reference Include="System, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.dll</HintPath>
</Reference>
<Reference Include="System.Xml, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Xml</HintPath>
</Reference>
<Reference Include="System.Net.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<HintPath>..\assemblies\System.Net.Http</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e">
<HintPath>..\assemblies\System.Runtime.Serialization</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Core.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Core.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Common, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Common.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Arch.Lifecycle.Runtime, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Arch.Lifecycle.Runtime.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Annotations, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Annotations.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.UI, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.UI.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Core.Utils, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Core.Utils.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Fragment, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Fragment.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.Media.Compat, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.Media.Compat.dll</HintPath>
</Reference>
<Reference Include="Xamarin.Android.Support.v4, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\Xamarin.Android.Support.v4.dll</HintPath>
</Reference>
<Reference Include="xTile, Version=1.0.7033.16602, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\assemblies\xTile.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Automate\Framework\AutomateAPI.cs" />
<Compile Include="Automate\Framework\AutomationFactory.cs" />
<Compile Include="Automate\Framework\BaseMachine.cs" />
<Compile Include="Automate\Framework\Connector.cs" />
<Compile Include="Automate\Framework\Consumable.cs" />
<Compile Include="Automate\Framework\GenericObjectMachine.cs" />
<Compile Include="Automate\Framework\MachineGroup.cs" />
<Compile Include="Automate\Framework\MachineGroupBuilder.cs" />
<Compile Include="Automate\Framework\MachineGroupFactory.cs" />
<Compile Include="Automate\Framework\Machines\Buildings\JunimoHutMachine.cs" />
<Compile Include="Automate\Framework\Machines\Buildings\MillMachine.cs" />
<Compile Include="Automate\Framework\Machines\Buildings\ShippingBinMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\AutoGrabberMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\BeeHouseMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\CaskMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\CharcoalKilnMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\CheesePressMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\CoopIncubatorMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\CrabPotMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\CrystalariumMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\FeedHopperMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\FurnaceMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\KegMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\LightningRodMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\LoomMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\MayonnaiseMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\MushroomBoxMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\OilMakerMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\PreservesJarMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\RecyclingMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\SeedMakerMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\SlimeEggPressMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\SlimeIncubatorMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\SodaMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\StatueOfEndlessFortuneMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\StatueOfPerfectionMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\TapperMachine.cs" />
<Compile Include="Automate\Framework\Machines\Objects\WormBinMachine.cs" />
<Compile Include="Automate\Framework\Machines\TerrainFeatures\FruitTreeMachine.cs" />
<Compile Include="Automate\Framework\Machines\Tiles\TrashCanMachine.cs" />
<Compile Include="Automate\Framework\Models\ModConfig.cs" />
<Compile Include="Automate\Framework\Models\ModConfigObject.cs" />
<Compile Include="Automate\Framework\Models\ObjectType.cs" />
<Compile Include="Automate\Framework\OverlayMenu.cs" />
<Compile Include="Automate\Framework\Recipe.cs" />
<Compile Include="Automate\Framework\StorageManager.cs" />
<Compile Include="Automate\Framework\Storage\ChestContainer.cs" />
<Compile Include="Automate\Framework\Storage\ContainerExtensions.cs" />
<Compile Include="Automate\IAutomatable.cs" />
<Compile Include="Automate\IAutomateAPI.cs" />
<Compile Include="Automate\IAutomationFactory.cs" />
<Compile Include="Automate\IConsumable.cs" />
<Compile Include="Automate\IContainer.cs" />
<Compile Include="Automate\IMachine.cs" />
<Compile Include="Automate\IRecipe.cs" />
<Compile Include="Automate\IStorage.cs" />
<Compile Include="Automate\ITrackedStack.cs" />
<Compile Include="Automate\MachineState.cs" />
<Compile Include="Automate\ModEntry.cs" />
<Compile Include="Automate\obj\x86\Debug\TemporaryGeneratedFile_036C0B5B-1481-4323-8D20-8F5ADCB23D92.cs" />
<Compile Include="Automate\obj\x86\Debug\TemporaryGeneratedFile_5937a670-0e60-4077-877b-f7221da3dda1.cs" />
<Compile Include="Automate\obj\x86\Debug\TemporaryGeneratedFile_E7A71F73-0F8D-4B9B-B56E-8E70B10BC5D3.cs" />
<Compile Include="Automate\TrackedItem.cs" />
<Compile Include="Automate\TrackedItemCollection.cs" />
<Compile Include="Common\CommonHelper.cs" />
<Compile Include="Common\DataParsers\CropDataParser.cs" />
<Compile Include="Common\Integrations\Automate\AutomateIntegration.cs" />
<Compile Include="Common\Integrations\Automate\IAutomateApi.cs" />
<Compile Include="Common\Integrations\BaseIntegration.cs" />
<Compile Include="Common\Integrations\BetterJunimos\BetterJunimosIntegration.cs" />
<Compile Include="Common\Integrations\BetterJunimos\IBetterJunimosApi.cs" />
<Compile Include="Common\Integrations\BetterSprinklers\BetterSprinklersIntegration.cs" />
<Compile Include="Common\Integrations\BetterSprinklers\IBetterSprinklersApi.cs" />
<Compile Include="Common\Integrations\Cobalt\CobaltIntegration.cs" />
<Compile Include="Common\Integrations\Cobalt\ICobaltApi.cs" />
<Compile Include="Common\Integrations\CustomFarmingRedux\CustomFarmingReduxIntegration.cs" />
<Compile Include="Common\Integrations\CustomFarmingRedux\ICustomFarmingApi.cs" />
<Compile Include="Common\Integrations\FarmExpansion\FarmExpansionIntegration.cs" />
<Compile Include="Common\Integrations\FarmExpansion\IFarmExpansionApi.cs" />
<Compile Include="Common\Integrations\IModIntegration.cs" />
<Compile Include="Common\Integrations\LineSprinklers\ILineSprinklersApi.cs" />
<Compile Include="Common\Integrations\LineSprinklers\LineSprinklersIntegration.cs" />
<Compile Include="Common\Integrations\PelicanFiber\PelicanFiberIntegration.cs" />
<Compile Include="Common\Integrations\PrismaticTools\IPrismaticToolsApi.cs" />
<Compile Include="Common\Integrations\PrismaticTools\PrismaticToolsIntegration.cs" />
<Compile Include="Common\Integrations\SimpleSprinkler\ISimplerSprinklerApi.cs" />
<Compile Include="Common\Integrations\SimpleSprinkler\SimpleSprinklerIntegration.cs" />
<Compile Include="Common\PathUtilities.cs" />
<Compile Include="Common\SpriteInfo.cs" />
<Compile Include="Common\StringEnumArrayConverter.cs" />
<Compile Include="Common\TileHelper.cs" />
<Compile Include="Common\UI\BaseOverlay.cs" />
<Compile Include="Common\UI\CommonSprites.cs" />
<Compile Include="Common\Utilities\ConstraintSet.cs" />
<Compile Include="Common\Utilities\InvariantDictionary.cs" />
<Compile Include="Common\Utilities\InvariantHashSet.cs" />
<Compile Include="Common\Utilities\ObjectReferenceComparer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Automate\screenshots\chests-anywhere-config.png" />
<Content Include="Automate\screenshots\connectors.png" />
<Content Include="Automate\screenshots\crab-pot-factory.png" />
<Content Include="Automate\screenshots\example-overlay.png" />
<Content Include="Automate\screenshots\extensibility-machine-groups.png" />
<Content Include="Automate\screenshots\iridium-bar-factory.png" />
<Content Include="Automate\screenshots\iridium-cheese-factory.png" />
<Content Include="Automate\screenshots\iridium-mead-factory.png" />
<Content Include="Automate\screenshots\refined-quartz-factory.png" />
</ItemGroup>
<ItemGroup>
<None Include="Automate\obj\x86\Debug\Automate.csproj.CoreCompileInputs.cache" />
<None Include="Automate\obj\x86\Debug\project.razor.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="Automate\bin\Debug\" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Common;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>The API which lets other mods interact with Automate.</summary>
public class AutomateAPI : IAutomateAPI
{
/*********
** Fields
*********/
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
/// <summary>Constructs machine groups.</summary>
private readonly MachineGroupFactory MachineGroupFactory;
/// <summary>The active machine groups recognised by Automate.</summary>
private readonly IDictionary<GameLocation, MachineGroup[]> ActiveMachineGroups;
/// <summary>The disabled machine groups recognised by Automate (e.g. machines not connected to a chest).</summary>
private readonly IDictionary<GameLocation, MachineGroup[]> DisabledMachineGroups;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="machineGroupFactory">Constructs machine groups.</param>
/// <param name="activeMachineGroups">The active machine groups recognised by Automate.</param>
/// <param name="disabledMachineGroups">The disabled machine groups recognised by Automate (e.g. machines not connected to a chest).</param>
internal AutomateAPI(IMonitor monitor, MachineGroupFactory machineGroupFactory, IDictionary<GameLocation, MachineGroup[]> activeMachineGroups, IDictionary<GameLocation, MachineGroup[]> disabledMachineGroups)
{
this.Monitor = monitor;
this.MachineGroupFactory = machineGroupFactory;
this.ActiveMachineGroups = activeMachineGroups;
this.DisabledMachineGroups = disabledMachineGroups;
}
/// <summary>Add an automation factory.</summary>
/// <param name="factory">An automation factory which construct machines, containers, and connectors.</param>
public void AddFactory(IAutomationFactory factory)
{
this.Monitor.Log($"Adding automation factory: {factory.GetType().AssemblyQualifiedName}", LogLevel.Trace);
this.MachineGroupFactory.Add(factory);
}
/// <summary>Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
public IDictionary<Vector2, int> GetMachineStates(GameLocation location, Rectangle tileArea)
{
IDictionary<Vector2, int> data = new Dictionary<Vector2, int>();
foreach (IMachine machine in this.GetMachineGroups(location).SelectMany(group => group.Machines))
{
if (machine.TileArea.Intersects(tileArea))
{
int state = (int)machine.GetState();
foreach (Vector2 tile in machine.TileArea.GetTiles())
{
if (tileArea.Contains((int)tile.X, (int)tile.Y))
data[tile] = state;
}
}
}
return data;
}
/*********
** Private methods
*********/
/// <summary>Get all machines in a location.</summary>
/// <param name="location">The location whose maches to fetch.</param>
private IEnumerable<MachineGroup> GetMachineGroups(GameLocation location)
{
// active groups
if (this.ActiveMachineGroups.TryGetValue(location, out MachineGroup[] activeGroups))
{
foreach (MachineGroup machineGroup in activeGroups)
yield return machineGroup;
}
// disabled groups
if (this.DisabledMachineGroups.TryGetValue(location, out MachineGroup[] disabledGroups))
{
foreach (MachineGroup machineGroup in disabledGroups)
yield return machineGroup;
}
}
}
}

View File

@ -0,0 +1,247 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Automate.Framework.Machines.Buildings;
using Pathoschild.Stardew.Automate.Framework.Machines.Objects;
using Pathoschild.Stardew.Automate.Framework.Machines.TerrainFeatures;
using Pathoschild.Stardew.Automate.Framework.Machines.Tiles;
using Pathoschild.Stardew.Automate.Framework.Models;
using Pathoschild.Stardew.Automate.Framework.Storage;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.Objects;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>Constructs machines, containers, or connectors which can be added to a machine group.</summary>
internal class AutomationFactory : IAutomationFactory
{
/*********
** Fields
*********/
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/// <summary>The object IDs through which machines can connect, but which have no other automation properties.</summary>
private readonly IDictionary<ObjectType, HashSet<int>> Connectors;
/// <summary>Whether to treat the shipping bin as a machine that can be automated.</summary>
private readonly bool AutomateShippingBin;
/// <summary>The tile area on the farm matching the shipping bin.</summary>
private readonly Rectangle ShippingBinArea = new Rectangle(71, 14, 2, 1);
/// <summary>Whether the Better Junimos mod is installed.</summary>
private readonly bool HasBetterJunimos;
/// <summary>Whether the Deluxe Auto-Grabber mod is installed.</summary>
private readonly bool HasDeluxeAutoGrabber;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="connectors">The objects through which machines can connect, but which have no other automation properties.</param>
/// <param name="automateShippingBin">Whether to treat the shipping bin as a machine that can be automated.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="reflection">Simplifies access to private game code.</param>
/// <param name="hasBetterJunimos">Whether the Better Junimos mod is installed.</param>
/// <param name="hasDeluxeAutoGrabber">Whether the Deluxe Auto-Grabber mod is installed.</param>
public AutomationFactory(ModConfigObject[] connectors, bool automateShippingBin, IMonitor monitor, IReflectionHelper reflection, bool hasBetterJunimos, bool hasDeluxeAutoGrabber)
{
this.Connectors = connectors
.GroupBy(connector => connector.Type)
.ToDictionary(group => group.Key, group => new HashSet<int>(group.Select(p => p.ID)));
this.AutomateShippingBin = automateShippingBin;
this.Monitor = monitor;
this.Reflection = reflection;
this.HasBetterJunimos = hasBetterJunimos;
this.HasDeluxeAutoGrabber = hasDeluxeAutoGrabber;
}
/// <summary>Get a machine, container, or connector instance for a given object.</summary>
/// <param name="obj">The in-game object.</param>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
public IAutomatable GetFor(SObject obj, GameLocation location, in Vector2 tile)
{
// chest container
if (obj is Chest chest)
return new ChestContainer(chest, location, tile);
// machine
if (obj.ParentSheetIndex == 165)
return new AutoGrabberMachine(obj, location, tile, ignoreSeedOutput: this.HasDeluxeAutoGrabber);
if (obj.name == "Bee House")
return new BeeHouseMachine(obj, location, tile);
if (obj is Cask cask)
return new CaskMachine(cask, location, tile);
if (obj.name == "Charcoal Kiln")
return new CharcoalKilnMachine(obj, location, tile);
if (obj.name == "Cheese Press")
return new CheesePressMachine(obj, location, tile);
if (obj is CrabPot pot)
return new CrabPotMachine(pot, location, tile, this.Monitor, this.Reflection);
if (obj.Name == "Crystalarium")
return new CrystalariumMachine(obj, location, tile, this.Reflection);
if (obj.name == "Feed Hopper")
return new FeedHopperMachine(location, tile);
if (obj.Name == "Furnace")
return new FurnaceMachine(obj, location, tile);
if (obj.name == "Incubator")
return new CoopIncubatorMachine(obj, location, tile);
if (obj.Name == "Keg")
return new KegMachine(obj, location, tile);
if (obj.name == "Lightning Rod")
return new LightningRodMachine(obj, location, tile);
if (obj.name == "Loom")
return new LoomMachine(obj, location, tile);
if (obj.name == "Mayonnaise Machine")
return new MayonnaiseMachine(obj, location, tile);
if (obj.Name == "Mushroom Box")
return new MushroomBoxMachine(obj, location, tile);
if (obj.name == "Oil Maker")
return new OilMakerMachine(obj, location, tile);
if (obj.name == "Preserves Jar")
return new PreservesJarMachine(obj, location, tile);
if (obj.name == "Recycling Machine")
return new RecyclingMachine(obj, location, tile);
if (obj.name == "Seed Maker")
return new SeedMakerMachine(obj, location, tile);
if (obj.name == "Slime Egg-Press")
return new SlimeEggPressMachine(obj, location, tile);
if (obj.name == "Slime Incubator")
return new SlimeIncubatorMachine(obj, location, tile);
if (obj.name == "Soda Machine")
return new SodaMachine(obj, location, tile);
if (obj.name == "Statue Of Endless Fortune")
return new StatueOfEndlessFortuneMachine(obj, location, tile);
if (obj.name == "Statue Of Perfection")
return new StatueOfPerfectionMachine(obj, location, tile);
if (obj.name == "Tapper")
{
if (location.terrainFeatures.TryGetValue(tile, out TerrainFeature terrainFeature) && terrainFeature is Tree tree)
return new TapperMachine(obj, location, tile, tree.treeType.Value);
}
if (obj.name == "Worm Bin")
return new WormBinMachine(obj, location, tile);
// connector
if (this.IsConnector(obj.bigCraftable.Value ? ObjectType.BigCraftable : ObjectType.Object, this.GetItemID(obj)))
return new Connector(location, tile);
return null;
}
/// <summary>Get a machine, container, or connector instance for a given terrain feature.</summary>
/// <param name="feature">The terrain feature.</param>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
public IAutomatable GetFor(TerrainFeature feature, GameLocation location, in Vector2 tile)
{
// machine
if (feature is FruitTree fruitTree)
return new FruitTreeMachine(fruitTree, location, tile);
// connector
if (feature is Flooring floor && this.IsConnector(ObjectType.Floor, floor.whichFloor.Value))
return new Connector(location, tile);
return null;
}
/// <summary>Get a machine, container, or connector instance for a given building.</summary>
/// <param name="building">The building.</param>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
public IAutomatable GetFor(Building building, BuildableGameLocation location, in Vector2 tile)
{
// machine
if (building is JunimoHut hut)
return new JunimoHutMachine(hut, location, ignoreSeedOutput: this.HasBetterJunimos);
if (building is Mill mill)
return new MillMachine(mill, location);
if (this.AutomateShippingBin && building is ShippingBin bin)
return new ShippingBinMachine(bin, location, Game1.getFarm());
if (building.buildingType.Value == "Silo")
return new FeedHopperMachine(building, location);
return null;
}
/// <summary>Get a machine, container, or connector instance for a given tile position.</summary>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
/// <remarks>Shipping bin logic from <see cref="Farm.leftClick"/>, garbage can logic from <see cref="Town.checkAction"/>.</remarks>
public IAutomatable GetForTile(GameLocation location, in Vector2 tile)
{
// shipping bin
if (this.AutomateShippingBin && location is Farm farm && (int)tile.X == this.ShippingBinArea.X && (int)tile.Y == this.ShippingBinArea.Y)
{
return new ShippingBinMachine(farm, this.ShippingBinArea);
}
// garbage can
if (location is Town town)
{
string action = town.doesTileHaveProperty((int)tile.X, (int)tile.Y, "Action", "Buildings");
if (!string.IsNullOrWhiteSpace(action) && action.StartsWith("Garbage ") && int.TryParse(action.Split(' ')[1], out int trashCanIndex))
return new TrashCanMachine(town, tile, trashCanIndex, this.Reflection);
}
return null;
}
/*********
** Private methods
*********/
/// <summary>Get whether a given object should be treated as a connector.</summary>
/// <param name="type">The object type.</param>
/// <param name="id">The object iD.</param>
private bool IsConnector(ObjectType type, int id)
{
return
this.Connectors.Count != 0
&& this.Connectors.TryGetValue(type, out HashSet<int> ids)
&& ids.Contains(id);
}
/// <summary>Get the object ID for a given object.</summary>
/// <param name="obj">The object instance.</param>
private int GetItemID(SObject obj)
{
// get object ID from fence ID
if (obj is Fence fence)
{
if (fence.isGate.Value)
return 325;
switch (fence.whichType.Value)
{
case Fence.wood:
return 322;
case Fence.stone:
return 323;
case Fence.steel:
return 324;
case Fence.gold:
return 298;
}
}
// else obj ID
return obj.ParentSheetIndex;
}
}
}

View File

@ -0,0 +1,90 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Buildings;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>The base implementation for a machine.</summary>
internal abstract class BaseMachine : IMachine
{
/*********
** Accessors
*********/
/// <summary>A unique ID for the machine type.</summary>
/// <remarks>This value should be identical for two machines if they have the exact same behavior and input logic. For example, if one machine in a group can't process input due to missing items, Automate will skip any other empty machines of that type in the same group since it assumes they need the same inputs.</remarks>
public string MachineTypeID { get; protected set; }
/// <summary>The location which contains the machine.</summary>
public GameLocation Location { get; }
/// <summary>The tile area covered by the machine.</summary>
public Rectangle TileArea { get; }
/*********
** Public methods
*********/
/// <summary>Get the machine's processing state.</summary>
public abstract MachineState GetState();
/// <summary>Get the output item.</summary>
public abstract ITrackedStack GetOutput();
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public abstract bool SetInput(IStorage input);
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="location">The machine's in-game location.</param>
/// <param name="tileArea">The tile area covered by the machine.</param>
protected BaseMachine(GameLocation location, in Rectangle tileArea)
{
this.MachineTypeID = this.GetType().FullName;
this.Location = location;
this.TileArea = tileArea;
}
/// <summary>Get the tile area for a building.</summary>
/// <param name="building">The building.</param>
protected static Rectangle GetTileAreaFor(Building building)
{
return new Rectangle(building.tileX.Value, building.tileY.Value, building.tilesWide.Value, building.tilesHigh.Value);
}
/// <summary>Get the tile area for a placed object.</summary>
/// <param name="tile">The tile position.</param>
protected static Rectangle GetTileAreaFor(in Vector2 tile)
{
return new Rectangle((int)tile.X, (int)tile.Y, 1, 1);
}
}
/// <summary>The base implementation for a machine.</summary>
internal abstract class BaseMachine<TMachine> : BaseMachine
{
/*********
** Fields
*********/
/// <summary>The underlying entity automated by this machine. This is only stored for the machine instance, and can be null if not applicable.</summary>
protected TMachine Machine { get; }
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying entity automated by this machine. This is only stored for the machine instance, and can be null if not applicable.</param>
/// <param name="location">The machine's in-game location.</param>
/// <param name="tileArea">The tile area covered by the machine.</param>
protected BaseMachine(TMachine machine, GameLocation location, in Rectangle tileArea)
: base(location, tileArea)
{
this.Machine = machine;
}
}
}

View File

@ -0,0 +1,37 @@
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>An entity which connects machines and chests in a machine group, but otherwise has no logic of its own.</summary>
internal class Connector : IAutomatable
{
/*********
** Accessors
*********/
/// <summary>The location which contains the machine.</summary>
public GameLocation Location { get; }
/// <summary>The tile area covered by the machine.</summary>
public Rectangle TileArea { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="location">The location which contains the machine.</param>
/// <param name="tileArea">The tile area covered by the machine.</param>
public Connector(GameLocation location, Rectangle tileArea)
{
this.Location = location;
this.TileArea = tileArea;
}
/// <summary>Construct an instance.</summary>
/// <param name="location">The location which contains the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public Connector(GameLocation location, Vector2 tile)
: this(location, new Rectangle((int)tile.X, (int)tile.Y, 1, 1)) { }
}
}

View File

@ -0,0 +1,50 @@
using StardewValley;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>An ingredient stack (or stacks) which can be consumed by a machine.</summary>
internal class Consumable : IConsumable
{
/*********
** Accessors
*********/
/// <summary>The items available to consumable.</summary>
public ITrackedStack Consumables { get; }
/// <summary>A sample item for comparison.</summary>
/// <remarks>This should not be a reference to the original stack.</remarks>
public Item Sample => this.Consumables.Sample;
/// <summary>The number of items needed for the recipe.</summary>
public int CountNeeded { get; }
/// <summary>Whether the consumables needed for this requirement are ready.</summary>
public bool IsMet { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="consumables">The matching items available to consume.</param>
/// <param name="countNeeded">The number of items needed for the recipe.</param>
public Consumable(ITrackedStack consumables, int countNeeded)
{
this.Consumables = consumables;
this.CountNeeded = countNeeded;
this.IsMet = consumables.Count >= countNeeded;
}
/// <summary>Remove the needed number of this item from the stack.</summary>
public void Reduce()
{
this.Consumables.Reduce(this.CountNeeded);
}
/// <summary>Remove the needed number of this item from the stack and return a new stack matching the count.</summary>
public Item Take()
{
return this.Consumables.Take(this.CountNeeded);
}
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>A generic machine instance.</summary>
internal abstract class GenericObjectMachine<TMachine> : BaseMachine<TMachine> where TMachine : SObject
{
/*********
** Public methods
*********/
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.Machine.heldObject.Value == null)
return MachineState.Empty;
return this.Machine.readyForHarvest.Value
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
return new TrackedItem(this.Machine.heldObject.Value, onEmpty: this.GenericReset);
}
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The in-game location.</param>
/// <param name="tile">The tile covered by the machine.</param>
protected GenericObjectMachine(TMachine machine, GameLocation location, Vector2 tile)
: base(machine, location, BaseMachine.GetTileAreaFor(tile)) { }
/// <summary>Reset the machine so it's ready to accept a new input.</summary>
/// <param name="item">The output item that was taken.</param>
protected void GenericReset(Item item)
{
this.Machine.heldObject.Value = null;
this.Machine.readyForHarvest.Value = false;
}
/// <summary>Generic logic to pull items from storage based on the given recipes.</summary>
/// <param name="storage">The available items.</param>
/// <param name="recipes">The recipes to match.</param>
protected bool GenericPullRecipe(IStorage storage, IRecipe[] recipes)
{
if (storage.TryGetIngredient(recipes, out IConsumable consumable, out IRecipe recipe))
{
this.Machine.heldObject.Value = recipe.Output(consumable.Take());
this.Machine.MinutesUntilReady = recipe.Minutes;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>A collection of machines and storage which work as one unit.</summary>
internal class MachineGroup
{
/*********
** Accessors
*********/
/// <summary>The location containing the group.</summary>
public GameLocation Location { get; }
/// <summary>The machines in the group.</summary>
public IMachine[] Machines { get; }
/// <summary>The containers in the group.</summary>
public IContainer[] Containers { get; }
/// <summary>The storage manager for the group.</summary>
public IStorage StorageManager { get; }
/// <summary>The tiles comprising the group.</summary>
public Vector2[] Tiles { get; }
/// <summary>Whether the group has the minimum requirements to enable internal automation (i.e., at least one chest and one machine).</summary>
public bool HasInternalAutomation => this.Machines.Length > 0 && this.Containers.Length > 0;
/*********
** Public methods
*********/
/// <summary>Create an instance.</summary>
/// <param name="location">The location containing the group.</param>
/// <param name="machines">The machines in the group.</param>
/// <param name="containers">The containers in the group.</param>
/// <param name="tiles">The tiles comprising the group.</param>
public MachineGroup(GameLocation location, IMachine[] machines, IContainer[] containers, Vector2[] tiles)
{
this.Location = location;
this.Machines = machines;
this.Containers = containers;
this.Tiles = tiles;
this.StorageManager = new StorageManager(containers);
}
/// <summary>Automate the machines inside the group.</summary>
public void Automate()
{
// get machines ready for input/output
IList<IMachine> outputReady = new List<IMachine>();
IList<IMachine> inputReady = new List<IMachine>();
foreach (IMachine machine in this.Machines)
{
switch (machine.GetState())
{
case MachineState.Done:
outputReady.Add(machine);
break;
case MachineState.Empty:
inputReady.Add(machine);
break;
}
}
if (!outputReady.Any() && !inputReady.Any())
return;
// process output
foreach (IMachine machine in outputReady)
{
if (this.StorageManager.TryPush(machine.GetOutput()) && machine.GetState() == MachineState.Empty)
inputReady.Add(machine);
}
// process input
HashSet<string> ignoreMachines = new HashSet<string>();
foreach (IMachine machine in inputReady)
{
if (ignoreMachines.Contains(machine.MachineTypeID))
continue;
if (!machine.SetInput(this.StorageManager))
ignoreMachines.Add(machine.MachineTypeID); // if the machine can't process available input, no need to ask every instance of its type
}
}
}
}

View File

@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Common;
using StardewValley;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>Handles logic for building a <see cref="MachineGroup"/>.</summary>
internal class MachineGroupBuilder
{
/*********
** Fields
*********/
/// <summary>The location containing the group.</summary>
private readonly GameLocation Location;
/// <summary>The machines in the group.</summary>
private readonly HashSet<IMachine> Machines = new HashSet<IMachine>();
/// <summary>The containers in the group.</summary>
private readonly HashSet<IContainer> Containers = new HashSet<IContainer>();
/// <summary>The tiles comprising the group.</summary>
private readonly HashSet<Vector2> Tiles = new HashSet<Vector2>();
/*********
** Accessors
*********/
/// <summary>The tile areas added to the machine group since the queue was last cleared.</summary>
internal IList<Rectangle> NewTileAreas { get; } = new List<Rectangle>();
/*********
** Public methods
*********/
/// <summary>Create an instance.</summary>
/// <param name="location">The location containing the group.</param>
public MachineGroupBuilder(GameLocation location)
{
this.Location = location;
}
/// <summary>Add a machine to the group.</summary>
/// <param name="machine">The machine to add.</param>
public void Add(IMachine machine)
{
this.Machines.Add(machine);
this.Add(machine.TileArea);
}
/// <summary>Add a container to the group.</summary>
/// <param name="container">The container to add.</param>
public void Add(IContainer container)
{
this.Containers.Add(container);
this.Add(container.TileArea);
}
/// <summary>Add connector tiles to the group.</summary>
/// <param name="tileArea">The tile area to add.</param>
public void Add(Rectangle tileArea)
{
foreach (Vector2 tile in tileArea.GetTiles())
this.Tiles.Add(tile);
this.NewTileAreas.Add(tileArea);
}
/// <summary>Get whether any tiles were added to the builder.</summary>
public bool HasTiles()
{
return this.Tiles.Count > 0;
}
/// <summary>Create a group from the saved data.</summary>
public MachineGroup Build()
{
return new MachineGroup(this.Location, this.Machines.ToArray(), this.Containers.ToArray(), this.Tiles.ToArray());
}
/// <summary>Clear the saved data.</summary>
public void Reset()
{
this.Machines.Clear();
this.Containers.Clear();
this.Tiles.Clear();
}
}
}

View File

@ -0,0 +1,176 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Automate.Framework.Storage;
using Pathoschild.Stardew.Common;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>Constructs machine groups.</summary>
internal class MachineGroupFactory
{
/*********
** Fields
*********/
/// <summary>The automation factories which construct machines, containers, and connectors.</summary>
private readonly IList<IAutomationFactory> AutomationFactories = new List<IAutomationFactory>();
/*********
** Public methods
*********/
/// <summary>Add an automation factory.</summary>
/// <param name="factory">An automation factory which construct machines, containers, and connectors.</param>
public void Add(IAutomationFactory factory)
{
this.AutomationFactories.Add(factory);
}
/// <summary>Get all machine groups in a location.</summary>
/// <param name="location">The location to search.</param>
public IEnumerable<MachineGroup> GetMachineGroups(GameLocation location)
{
MachineGroupBuilder builder = new MachineGroupBuilder(location);
ISet<Vector2> visited = new HashSet<Vector2>();
foreach (Vector2 tile in location.GetTiles())
{
this.FloodFillGroup(builder, location, tile, visited);
if (builder.HasTiles())
{
yield return builder.Build();
builder.Reset();
}
}
}
/*********
** Private methods
*********/
/// <summary>Extend the given machine group to include all machines and containers connected to the given tile, if any.</summary>
/// <param name="machineGroup">The machine group to extend.</param>
/// <param name="location">The location to search.</param>
/// <param name="origin">The first tile to check.</param>
/// <param name="visited">A lookup of visited tiles.</param>
private void FloodFillGroup(MachineGroupBuilder machineGroup, GameLocation location, in Vector2 origin, ISet<Vector2> visited)
{
// skip if already visited
if (visited.Contains(origin))
return;
// flood-fill connected machines & containers
Queue<Vector2> queue = new Queue<Vector2>();
queue.Enqueue(origin);
while (queue.Any())
{
// get tile
Vector2 tile = queue.Dequeue();
if (!visited.Add(tile))
continue;
// add machines, containers, or connectors which covers this tile
if (this.TryAddEntity(machineGroup, location, tile))
{
foreach (Rectangle tileArea in machineGroup.NewTileAreas)
{
// mark visited
foreach (Vector2 cur in tileArea.GetTiles())
visited.Add(cur);
// connect entities on surrounding tiles
foreach (Vector2 next in tileArea.GetSurroundingTiles())
{
if (!visited.Contains(next))
queue.Enqueue(next);
}
}
machineGroup.NewTileAreas.Clear();
}
}
}
/// <summary>Add any machine, container, or connector on the given tile to the machine group.</summary>
/// <param name="group">The machine group to extend.</param>
/// <param name="location">The location to search.</param>
/// <param name="tile">The tile to search.</param>
private bool TryAddEntity(MachineGroupBuilder group, GameLocation location, in Vector2 tile)
{
switch (this.GetEntity(location, tile))
{
case IMachine machine:
group.Add(machine);
return true;
case IContainer container:
if (!container.ShouldIgnore())
{
group.Add(container);
return true;
}
return false;
case IAutomatable connector:
group.Add(connector.TileArea);
return true;
default:
return false;
}
}
/// <summary>Get a machine, container, or connector from the given tile, if any.</summary>
/// <param name="location">The location to search.</param>
/// <param name="tile">The tile to search.</param>
private IAutomatable GetEntity(GameLocation location, Vector2 tile)
{
foreach (IAutomationFactory factory in this.AutomationFactories)
{
// from object
if (location.objects.TryGetValue(tile, out SObject obj))
{
IAutomatable entity = factory.GetFor(obj, location, tile);
if (entity != null)
return entity;
}
// from terrain feature
if (location.terrainFeatures.TryGetValue(tile, out TerrainFeature feature))
{
IAutomatable entity = factory.GetFor(feature, location, tile);
if (entity != null)
return entity;
}
// building machine
if (location is BuildableGameLocation buildableLocation)
{
foreach (Building building in buildableLocation.buildings)
{
Rectangle tileArea = new Rectangle(building.tileX.Value, building.tileY.Value, building.tilesWide.Value, building.tilesHigh.Value);
if (tileArea.Contains((int)tile.X, (int)tile.Y))
{
IAutomatable entity = factory.GetFor(building, buildableLocation, tile);
if (entity != null)
return entity;
}
}
}
// from tile position
{
IAutomatable entity = factory.GetForTile(location, tile);
if (entity != null)
return entity;
}
}
// none found
return null;
}
}
}

View File

@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Linq;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Objects;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Buildings
{
/// <summary>A Junimo hut machine that accepts input and provides output.</summary>
internal class JunimoHutMachine : BaseMachine<JunimoHut>
{
/*********
** Fields
*********/
/// <summary>Whether seeds should be ignored when selecting output.</summary>
private readonly bool IgnoreSeedOutput;
/// <summary>The Junimo hut's output chest.</summary>
private Chest Output => this.Machine.output.Value;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="hut">The underlying Junimo hut.</param>
/// <param name="location">The location which contains the machine.</param>
/// <param name="ignoreSeedOutput">Whether seeds should be ignored when selecting output.</param>
public JunimoHutMachine(JunimoHut hut, GameLocation location, bool ignoreSeedOutput)
: base(hut, location, BaseMachine.GetTileAreaFor(hut))
{
this.IgnoreSeedOutput = ignoreSeedOutput;
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.Output.items.Any(item => item != null))
return MachineState.Done;
return MachineState.Processing;
}
/// <summary>Get the machine output.</summary>
public override ITrackedStack GetOutput()
{
IList<Item> inventory = this.Output.items;
return new TrackedItem(inventory.FirstOrDefault(item => item != null), onEmpty: this.OnOutputTaken);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
/*********
** Private methods
*********/
/// <summary>Remove an output item once it's been taken.</summary>
/// <param name="item">The removed item.</param>
private void OnOutputTaken(Item item)
{
this.Output.clearNulls();
this.Output.items.Remove(item);
}
/// <summary>Get the next output item.</summary>
private Item GetNextOutput()
{
foreach (Item item in this.Output.items)
{
if (item == null)
continue;
if (this.IgnoreSeedOutput && (item as SObject)?.Category == SObject.SeedsCategory)
continue;
return item;
}
return null;
}
}
}

View File

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Objects;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Buildings
{
/// <summary>A mill machine that accepts input and provides output.</summary>
internal class MillMachine : BaseMachine<Mill>
{
/*********
** Fields
*********/
/// <summary>The mill's input chest.</summary>
private Chest Input => this.Machine.input.Value;
/// <summary>The mill's output chest.</summary>
private Chest Output => this.Machine.output.Value;
/// <summary>The maximum input stack size to allow per item ID, if different from <see cref="Item.maximumStackSize"/>.</summary>
private readonly IDictionary<int, int> MaxInputStackSize;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mill">The underlying mill.</param>
/// <param name="location">The location which contains the machine.</param>
public MillMachine(Mill mill, GameLocation location)
: base(mill, location, BaseMachine.GetTileAreaFor(mill))
{
this.MaxInputStackSize = new Dictionary<int, int>
{
[284] = new SObject(284, 1).maximumStackSize() / 3 // beet => 3 sugar (reduce stack to avoid overfilling output)
};
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.Output.items.Any(item => item != null))
return MachineState.Done;
return this.InputFull()
? MachineState.Processing
: MachineState.Empty; // 'empty' insofar as it will accept more input, not necessarily empty
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
IList<Item> inventory = this.Output.items;
return new TrackedItem(inventory.FirstOrDefault(item => item != null), onEmpty: this.OnOutputTaken);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
if (this.InputFull())
return false;
// fill input with wheat (262) and beets (284)
bool anyPulled = false;
foreach (ITrackedStack stack in input.GetItems().Where(i => i.Sample.ParentSheetIndex == 262 || i.Sample.ParentSheetIndex == 284))
{
// add item
bool anyAdded = this.TryAddInput(stack);
if (!anyAdded)
continue;
anyPulled = true;
// stop if full
if (this.InputFull())
return true;
}
return anyPulled;
}
/*********
** Private methods
*********/
/// <summary>Try to add an item to the input queue, and adjust its stack size accordingly.</summary>
/// <param name="item">The item stack to add.</param>
/// <returns>Returns whether any items were taken from the stack.</returns>
private bool TryAddInput(ITrackedStack item)
{
// nothing to add
if (item.Count <= 0)
return false;
// clean up input bin
this.Input.clearNulls();
// try adding to input
int originalSize = item.Count;
IList<Item> slots = this.Input.items;
int maxStackSize = this.GetMaxInputStackSize(item.Sample);
for (int i = 0; i < Chest.capacity; i++)
{
// done
if (item.Count <= 0)
break;
// add to existing slot
if (slots.Count > i)
{
Item slot = slots[i];
if (item.Sample.canStackWith(slot) && slot.Stack < maxStackSize)
{
int maxToAdd = Math.Min(item.Count, maxStackSize - slot.Stack); // the most items we can add to the stack (in theory)
int actualAdded = maxToAdd - slot.addToStack(maxToAdd); // how many items were actually added to the stack
item.Reduce(actualAdded);
}
continue;
}
// add to new slot
slots.Add(item.Take(Math.Min(item.Count, maxStackSize)));
}
return item.Count < originalSize;
}
/// <summary>Get whether the mill's input bin is full.</summary>
private bool InputFull()
{
var slots = this.Input.items;
// free slots
if (slots.Count < Chest.capacity)
return false;
// free space in stacks
foreach (Item slot in slots)
{
if (slot == null || slot.Stack < this.GetMaxInputStackSize(slot))
return false;
}
return true;
}
/*********
** Private methods
*********/
/// <summary>Remove an output item once it's been taken.</summary>
/// <param name="item">The removed item.</param>
private void OnOutputTaken(Item item)
{
this.Output.clearNulls();
this.Output.items.Remove(item);
}
/// <summary>Get the maximum input stack size to allow for an item.</summary>
/// <param name="item">The input item to check.</param>
private int GetMaxInputStackSize(Item item)
{
if (item == null)
return 0;
return this.MaxInputStackSize.TryGetValue(item.ParentSheetIndex, out int max)
? max
: item.maximumStackSize();
}
}
}

View File

@ -0,0 +1,70 @@
using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Buildings;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Buildings
{
/// <summary>A shipping bin that accepts input and provides output.</summary>
internal class ShippingBinMachine : BaseMachine
{
/*********
** Fields
*********/
/// <summary>The farm to automate.</summary>
private readonly Farm Farm;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="farm">The farm containing the shipping bin.</param>
/// <param name="tileArea">The tile area covered by the machine.</param>
public ShippingBinMachine(Farm farm, Rectangle tileArea)
: base(farm, tileArea)
{
this.Farm = farm;
}
/// <summary>Construct an instance.</summary>
/// <param name="bin">The constructed shipping bin.</param>
/// <param name="location">The location which contains the machine.</param>
/// <param name="farm">The farm which has the shipping bin data.</param>
public ShippingBinMachine(ShippingBin bin, GameLocation location, Farm farm)
: base(location, BaseMachine.GetTileAreaFor(bin))
{
this.Farm = farm;
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return MachineState.Empty; // always accepts items
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
return null; // no output
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
ITrackedStack tracker = input.GetItems().Where(p => p.Sample is SObject obj && obj.canBeShipped()).Take(1).FirstOrDefault();
if (tracker != null)
{
SObject item = (SObject)tracker.Take(tracker.Count);
this.Farm.shippingBin.Add(item);
this.Farm.lastItemShipped = item;
this.Farm.showShipment(item, false);
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,92 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Objects;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>An auto-grabber that provides output.</summary>
/// <remarks>See the game's default logic in <see cref="SObject.DayUpdate"/> and <see cref="SObject.checkForAction"/>.</remarks>
internal class AutoGrabberMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>Whether seeds should be ignored when selecting output.</summary>
private readonly bool IgnoreSeedOutput;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The in-game location.</param>
/// <param name="tile">The tile covered by the machine.</param>
/// <param name="ignoreSeedOutput">Whether seeds should be ignored when selecting output.</param>
public AutoGrabberMachine(SObject machine, GameLocation location, Vector2 tile, bool ignoreSeedOutput)
: base(machine, location, tile)
{
this.IgnoreSeedOutput = ignoreSeedOutput;
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return this.Machine.heldObject.Value is Chest output && this.GetNextOutput() != null
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
Item next = this.GetNextOutput();
return new TrackedItem(next, onEmpty: this.OnOutputTaken);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false;
}
/*********
** Private methods
*********/
/// <summary>Get the output chest.</summary>
private Chest GetOutputChest()
{
return (Chest)this.Machine.heldObject.Value;
}
/// <summary>Remove an output item once it's been taken.</summary>
/// <param name="item">The removed item.</param>
private void OnOutputTaken(Item item)
{
Chest output = this.GetOutputChest();
output.clearNulls();
output.items.Remove(item);
}
/// <summary>Get the next output item.</summary>
private Item GetNextOutput()
{
foreach (Item item in this.GetOutputChest().items)
{
if (item == null)
continue;
if (this.IgnoreSeedOutput && (item as SObject)?.Category == SObject.SeedsCategory)
continue;
return item;
}
return null;
}
}
}

View File

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A bee house that accepts input and provides output.</summary>
/// <remarks>See the game's machine logic in <see cref="SObject.performDropDownAction"/>, <see cref="SObject.checkForAction"/>, and <see cref="SObject.minutesElapsed"/>.</remarks>
internal class BeeHouseMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The honey types produced by this beehouse indexed by input ID.</summary>
private readonly IDictionary<int, SObject.HoneyType> HoneyTypes = new Dictionary<int, SObject.HoneyType>
{
[376] = SObject.HoneyType.Poppy,
[591] = SObject.HoneyType.Tulip,
[593] = SObject.HoneyType.SummerSpangle,
[595] = SObject.HoneyType.FairyRose,
[597] = SObject.HoneyType.BlueJazz
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public BeeHouseMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return Game1.currentSeason == "winter"
? MachineState.Disabled
: base.GetState();
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
// get raw output
SObject output = this.Machine.heldObject.Value;
if (output == null)
return null;
// get flower data
SObject.HoneyType type = SObject.HoneyType.Wild;
string prefix = type.ToString();
int addedPrice = 0;
Crop flower = Utility.findCloseFlower(this.Location, this.Machine.TileLocation);
if (flower != null)
{
string[] flowerData = Game1.objectInformation[flower.indexOfHarvest.Value].Split('/');
prefix = flowerData[0];
addedPrice = Convert.ToInt32(flowerData[1]) * 2;
if (!this.HoneyTypes.TryGetValue(flower.indexOfHarvest.Value, out type))
type = SObject.HoneyType.Wild;
}
// build object
SObject result = new SObject(output.ParentSheetIndex, output.Stack)
{
name = $"{prefix} Honey",
Price = output.Price + addedPrice
};
result.honeyType.Value = type;
// yield
return new TrackedItem(result, onEmpty: this.Reset);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input needed
}
/*********
** 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 Reset(Item item)
{
SObject machine = this.Machine;
machine.heldObject.Value = new SObject(Vector2.Zero, 340, null, false, true, false, false);
machine.MinutesUntilReady = 2400 - Game1.timeOfDay + 4320;
machine.readyForHarvest.Value = false;
machine.showNextIndex.Value = false;
}
}
}

View File

@ -0,0 +1,99 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Objects;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A cask that accepts input and provides output.</summary>
internal class CaskMachine : GenericObjectMachine<Cask>
{
/*********
** Fields
*********/
/// <summary>The items which can be aged in a cask with their aging rates.</summary>
private readonly IDictionary<int, float> AgingRates = new Dictionary<int, float>
{
[424] = 4, // cheese
[426] = 4, // goat cheese
[459] = 2, // mead
[303] = 1.66f, // pale ale
[346] = 2, // beer
[348] = 1 // wine
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public CaskMachine(Cask machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
SObject heldObject = this.Machine.heldObject.Value;
if (heldObject == null)
return MachineState.Empty;
return heldObject.Quality >= 4
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
Cask cask = this.Machine;
SObject heldObject = this.Machine.heldObject.Value;
return new TrackedItem(heldObject.getOne(), item =>
{
cask.heldObject.Value = null;
cask.MinutesUntilReady = 0;
cask.readyForHarvest.Value = false;
cask.agingRate.Value = 0;
cask.daysToMature.Value = 0;
});
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
Cask cask = this.Machine;
if (input.TryGetIngredient(match => (match.Sample as SObject)?.Quality < 4 && this.AgingRates.ContainsKey(match.Sample.ParentSheetIndex), 1, out IConsumable consumable))
{
SObject ingredient = (SObject)consumable.Take();
cask.heldObject.Value = ingredient;
cask.agingRate.Value = this.AgingRates[ingredient.ParentSheetIndex];
cask.daysToMature.Value = 56;
cask.MinutesUntilReady = 999999;
switch (ingredient.Quality)
{
case SObject.medQuality:
cask.daysToMature.Value = 42;
break;
case SObject.highQuality:
cask.daysToMature.Value = 28;
break;
case SObject.bestQuality:
cask.daysToMature.Value = 0;
cask.MinutesUntilReady = 1;
break;
}
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,49 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A charcoal kiln that accepts input and provides output.</summary>
internal class CharcoalKilnMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// wood => coal
new Recipe(
input: 388,
inputCount: 10,
output: input => new SObject(382, 1),
minutes: 30
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public CharcoalKilnMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
if (this.GenericPullRecipe(input, this.Recipes))
{
this.Machine.showNextIndex.Value = true;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,75 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A cheese press that accepts input and provides output.</summary>
internal class CheesePressMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes processed by this machine (input => output).</summary>
private readonly IRecipe[] Recipes =
{
// goat milk => goat cheese
new Recipe(
input: 436,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 426, null, false, true, false, false),
minutes: 200
),
// large goat milk => gold-quality goat cheese
new Recipe(
input: 438,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 426, null, false, true, false, false) { Quality = SObject.highQuality },
minutes: 200
),
// milk => cheese
new Recipe(
input: 184,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 424, null, false, true, false, false),
minutes: 200
),
// large milk => gold-quality cheese
new Recipe(
input: 186,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 424, "Cheese (=)", false, true, false, false) { Quality = SObject.highQuality },
minutes: 200
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public CheesePressMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
if (input.TryGetIngredient(this.Recipes, out IConsumable consumable, out IRecipe recipe))
{
this.Machine.heldObject.Value = recipe.Output(consumable.Take());
this.Machine.MinutesUntilReady = recipe.Minutes;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,80 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A coop incubator that accepts eggs and spawns chickens.</summary>
internal class CoopIncubatorMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public CoopIncubatorMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile)
{
int minutesUntilReady = Game1.player.professions.Contains(2) ? 9000 : 18000;
this.Recipes = new IRecipe[]
{
// egg => chicken
new Recipe(
input: -5,
inputCount: 1,
output: item => new SObject(item.ParentSheetIndex, 1),
minutes: minutesUntilReady / 2
),
// dinosaur egg => dinosaur
new Recipe(
input: 107,
inputCount: 1,
output: item => new SObject(107, 1),
minutes: minutesUntilReady
)
};
}
/// <summary>Get the machine's processing state.</summary>
/// <remarks>The coop incubator never produces an object output so it is never done.</remarks>
public override MachineState GetState()
{
return this.Machine.heldObject.Value != null
? MachineState.Processing
: MachineState.Empty;
}
/// <summary>Get the output item.</summary>
/// <remarks>The coop incubator never produces an object output.</remarks>
public override ITrackedStack GetOutput()
{
return null;
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
bool started = this.GenericPullRecipe(input, this.Recipes);
if (started)
{
int eggID = this.Machine.heldObject.Value.ParentSheetIndex;
this.Machine.ParentSheetIndex = eggID == 180 || eggID == 182 || eggID == 305
? this.Machine.ParentSheetIndex + 2
: this.Machine.ParentSheetIndex + 1;
}
return started;
}
}
}

View File

@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Objects;
using SFarmer = StardewValley.Farmer;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A crab pot that accepts input and provides output.</summary>
/// <remarks>See the game's machine logic in <see cref="CrabPot.DayUpdate"/> and <see cref="CrabPot.performObjectDropInAction"/>.</remarks>
internal class CrabPotMachine : GenericObjectMachine<CrabPot>
{
/*********
** Fields
*********/
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
/// <summary>The fish IDs for which any crabpot has logged an 'invalid fish data' error.</summary>
private static readonly ISet<int> LoggedInvalidDataErrors = new HashSet<int>();
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="reflection">Simplifies access to private game code.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="tile">The tile covered by the machine.</param>
public CrabPotMachine(CrabPot machine, GameLocation location, Vector2 tile, IMonitor monitor, IReflectionHelper reflection)
: base(machine, location, tile)
{
this.Monitor = monitor;
this.Reflection = reflection;
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.Machine.heldObject.Value == null)
{
bool hasBait = this.Machine.bait.Value != null || Game1.player.professions.Contains(11); // no bait needed if luremaster
return hasBait
? MachineState.Processing
: MachineState.Empty;
}
return this.Machine.readyForHarvest.Value
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
return new TrackedItem(this.Machine.heldObject.Value, onEmpty: this.Reset);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
// get bait
if (input.TryGetIngredient(SObject.baitCategory, 1, out IConsumable bait))
{
this.Machine.bait.Value = (SObject)bait.Take();
this.Reflection.GetField<bool>(this.Machine, "lidFlapping").SetValue(true);
this.Reflection.GetField<float>(this.Machine, "lidFlapTimer").SetValue(60);
return true;
}
return false;
}
/*********
** 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>
/// <remarks>XP and achievement logic based on <see cref="CrabPot.checkForAction"/>.</remarks>
private void Reset(Item item)
{
CrabPot pot = this.Machine;
// add fishing XP
Game1.player.gainExperience(SFarmer.fishingSkill, 5);
// mark fish caught for achievements and stats
IDictionary<int, string> fishData = Game1.content.Load<Dictionary<int, string>>("Data\\Fish");
if (fishData.TryGetValue(item.ParentSheetIndex, out string fishRow))
{
int size = 0;
try
{
string[] fields = fishRow.Split('/');
int lowerSize = fields.Length > 5 ? Convert.ToInt32(fields[5]) : 1;
int upperSize = fields.Length > 5 ? Convert.ToInt32(fields[6]) : 10;
size = Game1.random.Next(lowerSize, upperSize + 1);
}
catch (Exception ex)
{
// The fish length stats don't affect anything, so it's not worth notifying the
// user; just log one trace message per affected fish for troubleshooting.
if (CrabPotMachine.LoggedInvalidDataErrors.Add(item.ParentSheetIndex))
this.Monitor.Log($"The game's fish data has an invalid entry (#{item.ParentSheetIndex}: {fishData[item.ParentSheetIndex]}). Automated crabpots won't track fish length stats for that fish.\n{ex}", LogLevel.Trace);
}
Game1.player.caughtFish(item.ParentSheetIndex, size);
}
// reset pot
pot.readyForHarvest.Value = false;
pot.heldObject.Value = null;
pot.tileIndexToShow = 710;
pot.bait.Value = null;
this.Reflection.GetField<bool>(pot, "lidFlapping").SetValue(true);
this.Reflection.GetField<float>(pot, "lidFlapTimer").SetValue(60f);
this.Reflection.GetField<Vector2>(pot, "shake").SetValue(Vector2.Zero);
this.Reflection.GetField<float>(pot, "shakeTimer").SetValue(0f);
}
}
}

View File

@ -0,0 +1,63 @@
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A crystalarium that accepts input and provides output.</summary>
internal class CrystalariumMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="reflection">Simplifies access to private game code.</param>
/// <param name="tile">The tile covered by the machine.</param>
public CrystalariumMachine(SObject machine, GameLocation location, Vector2 tile, IReflectionHelper reflection)
: base(machine, location, tile)
{
this.Reflection = reflection;
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.Machine.heldObject.Value == null)
return MachineState.Disabled;
return this.Machine.readyForHarvest.Value
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
SObject machine = this.Machine;
SObject heldObject = machine.heldObject.Value;
return new TrackedItem(heldObject.getOne(), item =>
{
machine.MinutesUntilReady = this.Reflection.GetMethod(machine, "getMinutesForCrystalarium").Invoke<int>(heldObject.ParentSheetIndex);
machine.readyForHarvest.Value = false;
});
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // started manually
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Buildings;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A hay hopper that accepts input and provides output.</summary>
internal class FeedHopperMachine : BaseMachine
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public FeedHopperMachine(GameLocation location, Vector2 tile)
: base(location, BaseMachine.GetTileAreaFor(tile)) { }
/// <summary>Construct an instance.</summary>
/// <param name="silo">The silo to automate.</param>
/// <param name="location">The location containing the machine.</param>
public FeedHopperMachine(Building silo, GameLocation location)
: base(location, BaseMachine.GetTileAreaFor(silo)) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
Farm farm = Game1.getFarm();
return this.GetFreeSpace(farm) > 0
? MachineState.Empty // 'empty' insofar as it will accept more input, not necessarily empty
: MachineState.Disabled;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
return null;
}
/// <summary>Reset the machine so it's ready to accept a new input.</summary>
/// <param name="outputTaken">Whether the current output was taken.</param>
public void Reset(bool outputTaken)
{
// not applicable
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
Farm farm = Game1.getFarm();
// skip if full
if (this.GetFreeSpace(farm) <= 0)
return false;
// try to add hay (178) until full
bool anyPulled = false;
foreach (ITrackedStack stack in input.GetItems().Where(p => p.Sample.ParentSheetIndex == 178))
{
// get free space
int space = this.GetFreeSpace(farm);
if (space <= 0)
return anyPulled;
// pull hay
int maxToAdd = Math.Min(stack.Count, space);
int added = maxToAdd - farm.tryToAddHay(maxToAdd);
stack.Reduce(added);
if (added > 0)
anyPulled = true;
}
return anyPulled;
}
/*********
** 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)
{
return Utility.numSilos() * 240 - farm.piecesOfHay.Value;
}
}
}

View File

@ -0,0 +1,92 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A furnace that accepts input and provides output.</summary>
internal class FurnaceMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
/// <remarks>Derived from <see cref="SObject.performObjectDropInAction"/>.</remarks>
private readonly IRecipe[] Recipes =
{
// copper => copper bar
new Recipe(
input: SObject.copper,
inputCount: 5,
output: input => new SObject(Vector2.Zero, SObject.copperBar, 1),
minutes: 30
),
// iron => iron bar
new Recipe(
input: SObject.iron,
inputCount: 5,
output: input => new SObject(Vector2.Zero, SObject.ironBar, 1),
minutes: 120
),
// gold => gold bar
new Recipe(
input: SObject.gold,
inputCount: 5,
output: input => new SObject(Vector2.Zero, SObject.goldBar, 1),
minutes: 300
),
// iridium => iridium bar
new Recipe(
input: SObject.iridium,
inputCount: 5,
output: input => new SObject(Vector2.Zero, SObject.iridiumBar, 1),
minutes: 480
),
// quartz => refined quartz
new Recipe(
input: SObject.quartzIndex,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 338, 1),
minutes: 90
),
// refined quartz => refined quartz
new Recipe(
input: 82,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 338, 3),
minutes: 90
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public FurnaceMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
if (input.TryGetIngredient(SObject.coal, 1, out IConsumable coal) && this.GenericPullRecipe(input, this.Recipes))
{
coal.Reduce();
this.Machine.initializeLightSource(this.Machine.TileLocation);
this.Machine.showNextIndex.Value = true;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,103 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A keg that accepts input and provides output.</summary>
/// <remarks>See the game's machine logic in <see cref="SObject.performObjectDropInAction"/> and <see cref="SObject.checkForAction"/>.</remarks>
internal class KegMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// honey => mead
new Recipe(
input: 340,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 459, "Mead", false, true, false, false) { name = "Mead" },
minutes: 600
),
// coffee bean => coffee
new Recipe(
input: 433,
inputCount: 5,
output: input => new SObject(Vector2.Zero, 395, "Coffee", false, true, false, false) { name = "Coffee" },
minutes: 120
),
// wheat => beer
new Recipe(
input: 262,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 346, "Beer", false, true, false, false) { name = "Beer" },
minutes: 1750
),
// hops => pale ale
new Recipe(
input: 304,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 303, "Pale Ale", false, true, false, false) { name = "Pale Ale" },
minutes: 2250
),
// fruit => wine
new Recipe(
input: SObject.FruitsCategory,
inputCount: 1,
output: input =>
{
SObject wine = new SObject(Vector2.Zero, 348, input.Name + " Wine", false, true, false, false)
{
name = input.Name + " Wine",
Price = ((SObject)input).Price * 3
};
wine.preserve.Value = SObject.PreserveType.Wine;
wine.preservedParentSheetIndex.Value = input.ParentSheetIndex;
return wine;
},
minutes: 10000
),
new Recipe(
input: SObject.VegetableCategory,
inputCount: 1,
output: input =>
{
SObject juice = new SObject(Vector2.Zero, 350, input.Name + " Juice", false, true, false, false)
{
name = input.Name + " Juice",
Price = (int)(((SObject)input).Price * 2.25)
};
juice.preserve.Value = SObject.PreserveType.Juice;
juice.preservedParentSheetIndex.Value = input.ParentSheetIndex;
return juice;
},
minutes: 6000
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public KegMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return this.GenericPullRecipe(input, this.Recipes);
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A lightning rod that accepts input and provides output.</summary>
internal class LightningRodMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public LightningRodMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
SObject heldObject = this.Machine.heldObject.Value;
return new TrackedItem(heldObject.getOne(), onEmpty: this.GenericReset);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
}
}

View File

@ -0,0 +1,56 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A loom that accepts input and provides output.</summary>
internal class LoomMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// wool => cloth
new Recipe(
input: 440,
inputCount: 1,
output: item => new SObject(Vector2.Zero, 428, null, false, true, false, false),
minutes: 240
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public LoomMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
SObject machine = this.Machine;
return new TrackedItem(machine.heldObject.Value, item =>
{
machine.heldObject.Value = null;
machine.readyForHarvest.Value = false;
machine.showNextIndex.Value = false;
});
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return this.GenericPullRecipe(input, this.Recipes);
}
}
}

View File

@ -0,0 +1,86 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A mayonnaise that accepts input and provides output.</summary>
internal class MayonnaiseMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// void egg => void mayonnaise
new Recipe(
input: 305,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 308, null, false, true, false, false),
minutes: 180
),
// duck egg => duck mayonnaise
new Recipe(
input: 442,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 307, null, false, true, false, false),
minutes: 180
),
// white/brown egg => normal mayonnaise
new Recipe(
input: 176,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false),
minutes: 180
),
new Recipe(
input: 180,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false),
minutes: 180
),
// dinosaur or large white/brown egg => gold-quality mayonnaise
new Recipe(
input: 107,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false) { Quality = SObject.highQuality },
minutes: 180
),
new Recipe(
input: 174,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false) { Quality = SObject.highQuality },
minutes: 180
),
new Recipe(
input: 182,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 306, null, false, true, false, false) { Quality = SObject.highQuality },
minutes: 180
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public MayonnaiseMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return this.GenericPullRecipe(input, this.Recipes);
}
}
}

View File

@ -0,0 +1,54 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A mushroom box that accepts input and provides output.</summary>
internal class MushroomBoxMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public MushroomBoxMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return this.Machine.heldObject.Value != null && this.Machine.readyForHarvest.Value
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
return new TrackedItem(this.Machine.heldObject.Value, onEmpty: this.Reset);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input needed
}
/*********
** 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 Reset(Item item)
{
this.GenericReset(item);
this.Machine.showNextIndex.Value = false;
}
}
}

View File

@ -0,0 +1,68 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>An oil maker that accepts input and provides output.</summary>
internal class OilMakerMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// truffle => truffle oil
new Recipe(
input: 430,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 432, null, false, true, false, false),
minutes: 360
),
// sunflower seed => oil
new Recipe(
input: 431,
inputCount: 1,
output: input => new SObject(247, 1),
minutes: 3200
),
// corn => oil
new Recipe(
input: 270,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 247, null, false, true, false, false),
minutes: 1000
),
// sunflower => oil
new Recipe(
input: 421,
inputCount: 1,
output: input => new SObject(Vector2.Zero, 247, null, false, true, false, false),
minutes: 60
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public OilMakerMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return this.GenericPullRecipe(input, this.Recipes);
}
}
}

View File

@ -0,0 +1,74 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A preserves jar that accepts input and provides output.</summary>
/// <remarks>See the game's machine logic in <see cref="SObject.performObjectDropInAction"/> and <see cref="SObject.checkForAction"/>.</remarks>
internal class PreservesJarMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// fruit => jelly
new Recipe(
input: SObject.FruitsCategory,
inputCount: 1,
output: input =>
{
SObject jelly = new SObject(Vector2.Zero, 344, input.Name + " Jelly", false, true, false, false)
{
Price = 50 + ((SObject) input).Price * 2,
name = input.Name + " Jelly"
};
jelly.preserve.Value = SObject.PreserveType.Jelly;
jelly.preservedParentSheetIndex.Value = input.ParentSheetIndex;
return jelly;
},
minutes: 4000
),
// vegetable => pickled vegetable
new Recipe(
input: SObject.VegetableCategory,
inputCount: 1,
output: input =>
{
SObject item = new SObject(Vector2.Zero, 342, "Pickled " + input.Name, false, true, false, false)
{
Price = 50 + ((SObject) input).Price * 2,
name = "Pickled " + input.Name
};
item.preserve.Value = SObject.PreserveType.Pickle;
item.preservedParentSheetIndex.Value = input.ParentSheetIndex;
return item;
},
minutes: 4000
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public PreservesJarMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return this.GenericPullRecipe(input, this.Recipes);
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A recycling maching that accepts input and provides output.</summary>
/// <remarks>This differs slightly from the game implementation in that it uses a more random RNG, due to a C# limitation which prevents us from accessing machine info from the cached recipe output functions for use in the RNG seed.</remarks>
internal class RecyclingMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The RNG to use for randomising output.</summary>
private static readonly Random Random = new Random();
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes =
{
// trash => coal/iron ore/stone
new Recipe(
input: 168,
inputCount: 1,
output: input => new SObject(RecyclingMachine.Random.NextDouble() < 0.3 ? 382 : (RecyclingMachine.Random.NextDouble() < 0.3 ? 380 : 390), RecyclingMachine.Random.Next(1, 4)),
minutes: 60
),
// driftwood => coal/wood
new Recipe(
input: 169,
inputCount: 1,
output: input => new SObject(RecyclingMachine.Random.NextDouble() < 0.25 ? 382 : 388, RecyclingMachine.Random.Next(1, 4)),
minutes: 60
),
// broken glasses or broken CD => refined quartz
new Recipe(
input: 170,
inputCount: 1,
output: input => new SObject(338, 1),
minutes: 60
),
new Recipe(
input: 171,
inputCount: 1,
output: input => new SObject(338, 1),
minutes: 60
),
// soggy newspaper => cloth/torch
new Recipe(
input: 172,
inputCount: 1,
output: input => RecyclingMachine.Random.NextDouble() < 0.1 ? new SObject(428, 1) : new Torch(Vector2.Zero, 3),
minutes: 60
)
};
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public RecyclingMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
//Random random = new Random((int)Game1.uniqueIDForThisGame / 2 + (int)Game1.stats.DaysPlayed + Game1.timeOfDay + (int)machine.tileLocation.X * 200 + (int)machine.tileLocation.Y);
if (this.GenericPullRecipe(input, this.Recipes))
{
Game1.stats.PiecesOfTrashRecycled += 1;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A seed maker that accepts input and provides output.</summary>
internal class SeedMakerMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>A crop ID => seed ID lookup.</summary>
private static readonly IDictionary<int, int> CropSeedIDs = SeedMakerMachine.GetCropSeedIDs();
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public SeedMakerMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
SObject machine = this.Machine;
// crop => seeds
if (input.TryGetIngredient(this.IsValidCrop, 1, out IConsumable crop))
{
crop.Reduce();
int seedID = SeedMakerMachine.CropSeedIDs[crop.Sample.ParentSheetIndex];
Random random = new Random((int)Game1.stats.DaysPlayed + (int)Game1.uniqueIDForThisGame / 2 + (int)machine.TileLocation.X + (int)machine.TileLocation.Y * 77 + Game1.timeOfDay);
machine.heldObject.Value = new SObject(seedID, random.Next(1, 4));
if (random.NextDouble() < 0.005)
machine.heldObject.Value = new SObject(499, 1);
else if (random.NextDouble() < 0.02)
machine.heldObject.Value = new SObject(770, random.Next(1, 5));
machine.MinutesUntilReady = 20;
return true;
}
return false;
}
/*********
** Public methods
*********/
/// <summary>Get whether a given item is a crop compatible with the seed marker.</summary>
/// <param name="item">The item to check.</param>
public bool IsValidCrop(ITrackedStack item)
{
return
item.Sample.ParentSheetIndex != 433 // seed maker doesn't allow coffee beans
&& SeedMakerMachine.CropSeedIDs.ContainsKey(item.Sample.ParentSheetIndex);
}
/// <summary>Get a crop ID => seed ID lookup.</summary>
public static IDictionary<int, int> GetCropSeedIDs()
{
IDictionary<int, int> lookup = new Dictionary<int, int>();
IDictionary<int, string> cropData = Game1.content.Load<Dictionary<int, string>>("Data\\Crops");
foreach (KeyValuePair<int, string> entry in cropData)
{
int dataCropID = Convert.ToInt32(entry.Value.Split('/')[3]);
int dataSeedID = entry.Key;
if (!lookup.ContainsKey(dataCropID)) // if multiple crops have the same seed, use the earliest one (which is more likely the vanilla seed)
lookup[dataCropID] = dataSeedID;
}
return lookup;
}
}
}

View File

@ -0,0 +1,50 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A slime egg-press that accepts input and provides output.</summary>
internal class SlimeEggPressMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public SlimeEggPressMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
SObject heldObject = this.Machine.heldObject.Value;
return new TrackedItem(heldObject.getOne(), this.GenericReset);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
// slime => slime egg
if (input.TryConsume(766, 100))
{
int parentSheetIndex = 680;
if (Game1.random.NextDouble() < 0.05)
parentSheetIndex = 439;
else if (Game1.random.NextDouble() < 0.1)
parentSheetIndex = 437;
else if (Game1.random.NextDouble() < 0.25)
parentSheetIndex = 413;
this.Machine.heldObject.Value = new SObject(parentSheetIndex, 1);
this.Machine.MinutesUntilReady = 1200;
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,90 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A slime incubator that accepts slime eggs and spawns slime monsters.</summary>
internal class SlimeIncubatorMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The recipes to process.</summary>
private readonly IRecipe[] Recipes;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public SlimeIncubatorMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile)
{
int minutesUntilReady = Game1.player.professions.Contains(2) ? 2000 : 4000;
this.Recipes = new IRecipe[] {
// blue slime egg => object with parentSheetIndex of blue slime egg
new Recipe(
input: 413,
inputCount: 1,
output: input => new SObject(413,1),
minutes: minutesUntilReady
),
// red slime egg => object with parentSheetIndex of red slime egg
new Recipe(
input: 437,
inputCount: 1,
output: input => new SObject(437,1),
minutes: minutesUntilReady
),
// purple slime egg => object with parentSheetIndex of purple slime egg
new Recipe(
input: 439,
inputCount: 1,
output: input => new SObject(439,1),
minutes: minutesUntilReady
),
// green slime egg => object with parentSheetIndex of green slime egg
new Recipe(
input: 680,
inputCount: 1,
output: input => new SObject(680,1),
minutes: minutesUntilReady
)
};
}
/// <summary>Get the machine's processing state.</summary>
/// <remarks>The slime incubator does not produce an output object, so it is never done.</remarks>
public override MachineState GetState()
{
return this.Machine.heldObject.Value != null
? MachineState.Processing
: MachineState.Empty;
}
/// <summary>Get the output item.</summary>
/// <remarks>The slime incubator does not produce an output object.</remarks>
public override ITrackedStack GetOutput()
{
return null;
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
bool started = this.GenericPullRecipe(input, this.Recipes);
if (started)
this.Machine.ParentSheetIndex = 157;
return started;
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A soda machine that accepts input and provides output.</summary>
internal class SodaMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public SodaMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return this.Machine.heldObject.Value != null
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A statue of endless fortune that accepts input and provides output.</summary>
internal class StatueOfEndlessFortuneMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public StatueOfEndlessFortuneMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return this.Machine.heldObject.Value != null
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A statue of perfection that accepts input and provides output.</summary>
internal class StatueOfPerfectionMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public StatueOfPerfectionMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
return this.Machine.heldObject.Value != null
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
}
}

View File

@ -0,0 +1,84 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A tapper that accepts input and provides output.</summary>
internal class TapperMachine : GenericObjectMachine<SObject>
{
/*********
** Fields
*********/
/// <summary>The tree type.</summary>
private readonly int TreeType;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location to search.</param>
/// <param name="tile">The tile covered by the machine.</param>
/// <param name="treeType">The tree type being tapped.</param>
public TapperMachine(SObject machine, GameLocation location, Vector2 tile, int treeType)
: base(machine, location, tile)
{
this.TreeType = treeType;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
SObject heldObject = this.Machine.heldObject.Value;
return new TrackedItem(heldObject.getOne(), onEmpty: this.Reset);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
/*********
** 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 Reset(Item item)
{
SObject tapper = this.Machine;
switch (this.TreeType)
{
case 1:
tapper.heldObject.Value = new SObject(725, 1);
tapper.MinutesUntilReady = 13000 - Game1.timeOfDay;
break;
case 2:
tapper.heldObject.Value = new SObject(724, 1);
tapper.MinutesUntilReady = 16000 - Game1.timeOfDay;
break;
case 3:
tapper.heldObject.Value = new SObject(726, 1);
tapper.MinutesUntilReady = 10000 - Game1.timeOfDay;
break;
case 7:
tapper.heldObject.Value = new SObject(420, 1);
tapper.MinutesUntilReady = 3000 - Game1.timeOfDay;
if (!Game1.currentSeason.Equals("fall"))
{
tapper.heldObject.Value = new SObject(404, 1);
tapper.MinutesUntilReady = 6000 - Game1.timeOfDay;
}
break;
}
tapper.heldObject.Value = (SObject)tapper.heldObject.Value.getOne();
tapper.readyForHarvest.Value = false;
}
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.Xna.Framework;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Objects
{
/// <summary>A tapper that accepts input and provides output.</summary>
/// <remarks>See the game's machine logic in <see cref="SObject.performDropDownAction"/> and <see cref="SObject.checkForAction"/>.</remarks>
internal class WormBinMachine : GenericObjectMachine<SObject>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="machine">The underlying machine.</param>
/// <param name="location">The location containing the machine.</param>
/// <param name="tile">The tile covered by the machine.</param>
public WormBinMachine(SObject machine, GameLocation location, Vector2 tile)
: base(machine, location, tile) { }
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
SObject bin = this.Machine;
return new TrackedItem(bin.heldObject.Value, item =>
{
bin.heldObject.Value = new SObject(685, Game1.random.Next(2, 6));
bin.MinutesUntilReady = 2600 - Game1.timeOfDay;
bin.readyForHarvest.Value = false;
bin.showNextIndex.Value = false;
});
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
}
}

View File

@ -0,0 +1,72 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework.Machines.TerrainFeatures
{
/// <summary>A fruit tree machine that accepts input and provides output.</summary>
/// <remarks>Derived from <see cref="FruitTree.shake"/>.</remarks>
internal class FruitTreeMachine : BaseMachine<FruitTree>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="tree">The underlying fruit tree.</param>
/// <param name="location">The machine's in-game location.</param>
/// <param name="tile">The tree's tile position.</param>
public FruitTreeMachine(FruitTree tree, GameLocation location, Vector2 tile)
: base(tree, location, BaseMachine.GetTileAreaFor(tile)) { }
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.Machine.growthStage.Value < FruitTree.treeStage)
return MachineState.Disabled;
return this.Machine.fruitsOnTree.Value > 0
? MachineState.Done
: MachineState.Processing;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
FruitTree tree = this.Machine;
// if struck by lightning => coal
if (tree.struckByLightningCountdown.Value > 0)
return new TrackedItem(new SObject(382, tree.fruitsOnTree.Value), onReduced: this.OnOutputReduced);
// else => fruit
int quality = SObject.lowQuality;
if (tree.daysUntilMature.Value <= -112)
quality = SObject.medQuality;
if (tree.daysUntilMature.Value <= -224)
quality = SObject.highQuality;
if (tree.daysUntilMature.Value <= -336)
quality = SObject.bestQuality;
return new TrackedItem(new SObject(tree.indexOfFruit.Value, tree.fruitsOnTree.Value, quality: quality), onReduced: this.OnOutputReduced);
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
/*********
** 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)
{
this.Machine.fruitsOnTree.Value = item.Stack;
}
}
}

View File

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Locations;
namespace Pathoschild.Stardew.Automate.Framework.Machines.Tiles
{
/// <summary>A trash can that accepts input and provides output.</summary>
internal class TrashCanMachine : BaseMachine
{
/*********
** Fields
*********/
/// <summary>The machine's position in its location.</summary>
private readonly Vector2 Tile;
/// <summary>The game's list of trash cans the player has already checked.</summary>
private readonly IList<bool> TrashCansChecked;
/// <summary>The trash can index (or -1 if not a valid trash can).</summary>
private readonly int TrashCanIndex = -1;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="town">The town to search.</param>
/// <param name="tile">The machine's position in its location.</param>
/// <param name="trashCanIndex">The trash can index.</param>
/// <param name="reflection">Simplifies access to private game code.</param>
public TrashCanMachine(Town town, Vector2 tile, int trashCanIndex, IReflectionHelper reflection)
: base(town, BaseMachine.GetTileAreaFor(tile))
{
this.Tile = tile;
this.TrashCansChecked = reflection.GetField<IList<bool>>(town, "garbageChecked").GetValue();
if (trashCanIndex >= 0 && trashCanIndex < this.TrashCansChecked.Count)
this.TrashCanIndex = trashCanIndex;
}
/// <summary>Get the machine's processing state.</summary>
public override MachineState GetState()
{
if (this.TrashCanIndex == -1)
return MachineState.Disabled;
if (this.TrashCansChecked[this.TrashCanIndex])
return MachineState.Processing;
return MachineState.Done;
}
/// <summary>Get the output item.</summary>
public override ITrackedStack GetOutput()
{
// get trash
int? itemID = this.GetRandomTrash(this.TrashCanIndex);
if (itemID.HasValue)
return new TrackedItem(new StardewValley.Object(itemID.Value, 1), onEmpty: this.MarkChecked);
// if nothing is returned, mark trash can checked
this.MarkChecked(null);
return null;
}
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
public override bool SetInput(IStorage input)
{
return false; // no input
}
/*********
** Private methods
*********/
/// <summary>Reset the machine so it starts processing the next item.</summary>
/// <param name="item">The output item that was taken.</param>
private void MarkChecked(Item item)
{
this.TrashCansChecked[this.TrashCanIndex] = true;
}
/// <summary>Get a random trash item ID.</summary>
/// <param name="index">The trash can index.</param>
/// <remarks>Duplicated from <see cref="Town.checkAction"/>.</remarks>
private int? GetRandomTrash(int index)
{
Random random = new Random((int)Game1.uniqueIDForThisGame / 2 + (int)Game1.stats.DaysPlayed + 777 + index);
if (random.NextDouble() < 0.2 + Game1.dailyLuck)
{
int parentSheetIndex = 168;
switch (random.Next(10))
{
case 0:
parentSheetIndex = 168;
break;
case 1:
parentSheetIndex = 167;
break;
case 2:
parentSheetIndex = 170;
break;
case 3:
parentSheetIndex = 171;
break;
case 4:
parentSheetIndex = 172;
break;
case 5:
parentSheetIndex = 216;
break;
case 6:
parentSheetIndex = Utility.getRandomItemFromSeason(Game1.currentSeason, ((int)this.Tile.X) * 653 + ((int)this.Tile.Y) * 777, false);
break;
case 7:
parentSheetIndex = 403;
break;
case 8:
parentSheetIndex = 309 + random.Next(3);
break;
case 9:
parentSheetIndex = 153;
break;
}
if (index == 3 && random.NextDouble() < 0.2 + Game1.dailyLuck)
{
parentSheetIndex = 535;
if (random.NextDouble() < 0.05)
parentSheetIndex = 749;
}
if (index == 4 && random.NextDouble() < 0.2 + Game1.dailyLuck)
{
parentSheetIndex = 378 + random.Next(3) * 2;
random.Next(1, 5);
}
if (index == 5 && random.NextDouble() < 0.2 + Game1.dailyLuck && Game1.dishOfTheDay != null)
parentSheetIndex = Game1.dishOfTheDay.ParentSheetIndex != 217 ? Game1.dishOfTheDay.ParentSheetIndex : 216;
if (index == 6 && random.NextDouble() < 0.2 + Game1.dailyLuck)
parentSheetIndex = 223;
return parentSheetIndex;
}
return null;
}
}
}

View File

@ -0,0 +1,37 @@
using Newtonsoft.Json;
using Pathoschild.Stardew.Common;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Automate.Framework.Models
{
/// <summary>The raw mod configuration.</summary>
internal class ModConfig
{
/*********
** Accessors
*********/
/// <summary>Whether to treat the shipping bin as a machine that can be automated.</summary>
public bool AutomateShippingBin { get; set; } = true;
/// <summary>The number of ticks between each automation process (60 = once per second).</summary>
public int AutomationInterval { get; set; } = 60;
/// <summary>The control bindings.</summary>
public ModConfigControls Controls { get; set; } = new ModConfigControls();
/// <summary>The in-game objects through which machines can connect.</summary>
public ModConfigObject[] Connectors { get; set; } = new ModConfigObject[0];
/*********
** Nested models
*********/
/// <summary>A set of control bindings.</summary>
internal class ModConfigControls
{
/// <summary>The button which toggles the automation overlay.</summary>
[JsonConverter(typeof(StringEnumArrayConverter))]
public SButton[] ToggleOverlay { get; set; } = { SButton.U };
}
}
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Pathoschild.Stardew.Automate.Framework.Models
{
/// <summary>An object identifier.</summary>
internal class ModConfigObject
{
/// <summary>The object type.</summary>
[JsonConverter(typeof(StringEnumConverter))]
public ObjectType Type { get; set; }
/// <summary>The object ID.</summary>
public int ID { get; set; }
}
}

View File

@ -0,0 +1,15 @@
namespace Pathoschild.Stardew.Automate.Framework.Models
{
/// <summary>The type of an in-game object for the mod's purposes.</summary>
internal enum ObjectType
{
/// <summary>A flooring object.</summary>
Floor,
/// <summary>A bigcraftable object.</summary>
BigCraftable,
/// <summary>A map object.</summary>
Object
}
}

View File

@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.Common.UI;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>The overlay which highlights automatable machines.</summary>
internal class OverlayMenu : BaseOverlay
{
/*********
** Fields
*********/
/// <summary>The padding to apply to tile backgrounds to make the grid visible.</summary>
private readonly int TileGap = 1;
/// <summary>A machine group lookup by tile coordinate.</summary>
private readonly IDictionary<Vector2, MachineGroup> GroupTiles;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="events">The SMAPI events available for mods.</param>
/// <param name="inputHelper">An API for checking and changing input state.</param>
/// <param name="machineGroups">The machine groups to display.</param>
public OverlayMenu(IModEvents events, IInputHelper inputHelper, IEnumerable<MachineGroup> machineGroups)
: base(events, inputHelper)
{
// init machine groups
machineGroups = machineGroups.ToArray();
this.GroupTiles =
(
from machineGroup in machineGroups
from tile in machineGroup.Tiles
select new { tile, machineGroup }
)
.ToDictionary(p => p.tile, p => p.machineGroup);
}
/*********
** Protected
*********/
/// <summary>Draw the overlay to the screen.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
[SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "Deliberate discarded for conversion to tile coordinates.")]
protected override void Draw(SpriteBatch spriteBatch)
{
if (!Context.IsPlayerFree)
return;
// draw each tile
foreach (Vector2 tile in TileHelper.GetVisibleTiles())
{
// get tile's screen coordinates
float screenX = tile.X * Game1.tileSize - Game1.viewport.X;
float screenY = tile.Y * Game1.tileSize - Game1.viewport.Y;
int tileSize = Game1.tileSize;
// get machine group
this.GroupTiles.TryGetValue(tile, out MachineGroup group);
bool isGrouped = group != null;
bool isActive = isGrouped && group.HasInternalAutomation;
// draw background
{
Color color = Color.Black * 0.5f;
if (isActive)
color = Color.Green * 0.2f;
else if (isGrouped)
color = Color.Red * 0.2f;
spriteBatch.DrawLine(screenX + this.TileGap, screenY + this.TileGap, new Vector2(tileSize - this.TileGap * 2, tileSize - this.TileGap * 2), color);
}
// draw group edge borders
if (group != null)
this.DrawEdgeBorders(spriteBatch, group, tile, group.HasInternalAutomation ? Color.Green : Color.Red);
}
// draw cursor
this.DrawCursor();
}
/*********
** Private methods
*********/
/// <summary>Draw borders for each unconnected edge of a tile.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="group">The machine group.</param>
/// <param name="tile">The group tile.</param>
/// <param name="color">The border color.</param>
private void DrawEdgeBorders(SpriteBatch spriteBatch, MachineGroup group, Vector2 tile, Color color)
{
int borderSize = 3;
float screenX = tile.X * Game1.tileSize - Game1.viewport.X;
float screenY = tile.Y * Game1.tileSize - Game1.viewport.Y;
float tileSize = Game1.tileSize;
// top
if (!group.Tiles.Contains(new Vector2(tile.X, tile.Y - 1)))
spriteBatch.DrawLine(screenX, screenY, new Vector2(tileSize, borderSize), color); // top
// bottom
if (!group.Tiles.Contains(new Vector2(tile.X, tile.Y + 1)))
spriteBatch.DrawLine(screenX, screenY + tileSize, new Vector2(tileSize, borderSize), color); // bottom
// left
if (!group.Tiles.Contains(new Vector2(tile.X - 1, tile.Y)))
spriteBatch.DrawLine(screenX, screenY, new Vector2(borderSize, tileSize), color); // left
// right
if (!group.Tiles.Contains(new Vector2(tile.X + 1, tile.Y)))
spriteBatch.DrawLine(screenX + tileSize, screenY, new Vector2(borderSize, tileSize), color); // right
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>Describes a generic recipe based on item input and output.</summary>
internal class Recipe : IRecipe
{
/*********
** Accessors
*********/
/// <summary>The input item or category ID.</summary>
public int InputID { get; }
/// <summary>The number of inputs needed.</summary>
public int InputCount { get; }
/// <summary>The output to generate (given an input).</summary>
public Func<Item, SObject> Output { get; }
/// <summary>The time needed to prepare an output.</summary>
public int Minutes { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="input">The input item or category ID.</param>
/// <param name="inputCount">The number of inputs needed.</param>
/// <param name="output">The output to generate (given an input).</param>
/// <param name="minutes">The time needed to prepare an output.</param>
public Recipe(int input, int inputCount, Func<Item, SObject> output, int minutes)
{
this.InputID = input;
this.InputCount = inputCount;
this.Output = output;
this.Minutes = minutes;
}
/// <summary>Get whether the recipe can accept a given item as input (regardless of stack size).</summary>
/// <param name="stack">The item to check.</param>
public bool AcceptsInput(ITrackedStack stack)
{
return stack.Sample.ParentSheetIndex == this.InputID || stack.Sample.Category == this.InputID;
}
}
}

View File

@ -0,0 +1,146 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Objects;
namespace Pathoschild.Stardew.Automate.Framework.Storage
{
/// <summary>A in-game chest which can provide or store items.</summary>
internal class ChestContainer : IContainer
{
/*********
** Fields
*********/
/// <summary>The underlying chest.</summary>
private readonly Chest Chest;
/*********
** Accessors
*********/
/// <summary>The container name (if any).</summary>
public string Name => this.Chest.Name;
/// <summary>The location which contains the container.</summary>
public GameLocation Location { get; }
/// <summary>The tile area covered by the container.</summary>
public Rectangle TileArea { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="chest">The underlying chest.</param>
/// <param name="location">The location which contains the container.</param>
/// <param name="tile">The tile area covered by the container.</param>
public ChestContainer(Chest chest, GameLocation location, Vector2 tile)
{
this.Chest = chest;
this.Location = location;
this.TileArea = new Rectangle((int)tile.X, (int)tile.Y, 1, 1);
}
/// <summary>Store an item stack.</summary>
/// <param name="stack">The item stack to store.</param>
/// <remarks>If the storage can't hold the entire stack, it should reduce the tracked stack accordingly.</remarks>
public void Store(ITrackedStack stack)
{
if (stack.Count <= 0)
return;
IList<Item> inventory = this.Chest.items;
// try stack into existing slot
foreach (Item slot in inventory)
{
if (slot != null && stack.Sample.canStackWith(slot))
{
int added = stack.Count - slot.addToStack(stack.Count);
stack.Reduce(added);
if (stack.Count <= 0)
return;
}
}
// try add to empty slot
for (int i = 0; i < Chest.capacity && i < inventory.Count; i++)
{
if (inventory[i] == null)
{
inventory[i] = stack.Take(stack.Count);
return;
}
}
// try add new slot
if (inventory.Count < Chest.capacity)
inventory.Add(stack.Take(stack.Count));
}
/// <summary>Find items in the pipe matching a predicate.</summary>
/// <param name="predicate">Matches items that should be returned.</param>
/// <param name="count">The number of items to find.</param>
/// <returns>If the pipe has no matching item, returns <c>null</c>. Otherwise returns a tracked item stack, which may have less items than requested if no more were found.</returns>
public ITrackedStack Get(Func<Item, bool> predicate, int count)
{
ITrackedStack[] stacks = this.GetImpl(predicate, count).ToArray();
if (!stacks.Any())
return null;
return new TrackedItemCollection(stacks);
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<ITrackedStack> GetEnumerator()
{
foreach (Item item in this.Chest.items.ToArray())
{
if (item != null)
yield return this.GetTrackedItem(item);
}
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/*********
** Private methods
*********/
/// <summary>Find items in the pipe matching a predicate.</summary>
/// <param name="predicate">Matches items that should be returned.</param>
/// <param name="count">The number of items to find.</param>
/// <remarks>If there aren't enough items in the pipe, it should return those it has.</remarks>
private IEnumerable<ITrackedStack> GetImpl(Func<Item, bool> predicate, int count)
{
int countFound = 0;
foreach (Item item in this.Chest.items)
{
if (item != null && predicate(item))
{
countFound += item.Stack;
yield return this.GetTrackedItem(item);
if (countFound >= count)
yield break;
}
}
}
/// <summary>Get a tracked item sync'd with the chest inventory.</summary>
/// <param name="item">The item to track.</param>
private ITrackedStack GetTrackedItem(Item item)
{
return new TrackedItem(item, onEmpty: i => this.Chest.items.Remove(i));
}
}
}

View File

@ -0,0 +1,47 @@
using System;
namespace Pathoschild.Stardew.Automate.Framework.Storage
{
/// <summary>Provides extensions for <see cref="IContainer"/> instances.</summary>
internal static class ContainerExtensions
{
/*********
** Public methods
*********/
/// <summary>Get whether the container name contains a given tag.</summary>
/// <param name="container">The container instance.</param>
/// <param name="tag">The tag to check, excluding the '|' delimiters.</param>
public static bool HasTag(this IContainer container, string tag)
{
return container.Name?.IndexOf($"|{tag}|", StringComparison.InvariantCultureIgnoreCase) >= 0;
}
/// <summary>Get whether this container should be preferred for output when possible.</summary>
/// <param name="container">The container instance.</param>
public static bool ShouldIgnore(this IContainer container)
{
return container.HasTag("automate:ignore");
}
/// <summary>Get whether input is enabled for this container.</summary>
/// <param name="container">The container instance.</param>
public static bool AllowsInput(this IContainer container)
{
return !container.ShouldIgnore() && !container.HasTag("automate:noinput");
}
/// <summary>Get whether output is enabled for this container.</summary>
/// <param name="container">The container instance.</param>
public static bool AllowsOutput(this IContainer container)
{
return !container.ShouldIgnore() && !container.HasTag("automate:nooutput");
}
/// <summary>Get whether this container should be preferred for output when possible.</summary>
/// <param name="container">The container instance.</param>
public static bool PreferForOutput(this IContainer container)
{
return container.HasTag("automate:output");
}
}
}

View File

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Pathoschild.Stardew.Automate.Framework.Storage;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate.Framework
{
/// <summary>Manages access to items in the underlying containers.</summary>
internal class StorageManager : IStorage
{
/*********
** Fields
*********/
/// <summary>The storage containers.</summary>
private readonly IContainer[] Containers;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="containers">The storage containers.</param>
public StorageManager(IEnumerable<IContainer> containers)
{
this.Containers = containers.ToArray();
}
/****
** GetItems
****/
/// <summary>Get all items from the given pipes.</summary>
public IEnumerable<ITrackedStack> GetItems()
{
foreach (IContainer container in this.Containers)
{
if (!container.AllowsOutput())
continue;
foreach (ITrackedStack item in container)
yield return item;
}
}
/****
** TryGetIngredient
****/
/// <summary>Get an ingredient needed for a recipe.</summary>
/// <param name="predicate">Returns whether an item should be matched.</param>
/// <param name="count">The number of items to find.</param>
/// <param name="consumable">The matching consumables.</param>
/// <returns>Returns whether the requirement is met.</returns>
public bool TryGetIngredient(Func<ITrackedStack, bool> predicate, int count, out IConsumable consumable)
{
int countMissing = count;
ITrackedStack[] consumables = this.GetItems().Where(predicate)
.TakeWhile(chestItem =>
{
if (countMissing <= 0)
return false;
countMissing -= chestItem.Count;
return true;
})
.ToArray();
consumable = new Consumable(new TrackedItemCollection(consumables), count);
return consumable.IsMet;
}
/// <summary>Get an ingredient needed for a recipe.</summary>
/// <param name="id">The item or category ID.</param>
/// <param name="count">The number of items to find.</param>
/// <param name="consumable">The matching consumables.</param>
/// <returns>Returns whether the requirement is met.</returns>
public bool TryGetIngredient(int id, int count, out IConsumable consumable)
{
return this.TryGetIngredient(item => item.Sample.ParentSheetIndex == id || item.Sample.Category == id, count, out consumable);
}
/// <summary>Get an ingredient needed for a recipe.</summary>
/// <param name="recipes">The items to match.</param>
/// <param name="consumable">The matching consumables.</param>
/// <param name="recipe">The matched requisition.</param>
/// <returns>Returns whether the requirement is met.</returns>
public bool TryGetIngredient(IRecipe[] recipes, out IConsumable consumable, out IRecipe recipe)
{
IDictionary<IRecipe, List<ITrackedStack>> accumulator = recipes.ToDictionary(req => req, req => new List<ITrackedStack>());
foreach (ITrackedStack stack in this.GetItems())
{
foreach (var entry in accumulator)
{
recipe = entry.Key;
List<ITrackedStack> found = entry.Value;
if (recipe.AcceptsInput(stack))
{
found.Add(stack);
if (found.Sum(p => p.Count) >= recipe.InputCount)
{
consumable = new Consumable(new TrackedItemCollection(found), entry.Key.InputCount);
return true;
}
}
}
}
consumable = null;
recipe = null;
return false;
}
/****
** TryConsume
****/
/// <summary>Consume an ingredient needed for a recipe.</summary>
/// <param name="predicate">Returns whether an item should be matched.</param>
/// <param name="count">The number of items to find.</param>
/// <returns>Returns whether the item was consumed.</returns>
public bool TryConsume(Func<ITrackedStack, bool> predicate, int count)
{
if (this.TryGetIngredient(predicate, count, out IConsumable requirement))
{
requirement.Reduce();
return true;
}
return false;
}
/// <summary>Consume an ingredient needed for a recipe.</summary>
/// <param name="itemID">The item ID.</param>
/// <param name="count">The number of items to find.</param>
/// <returns>Returns whether the item was consumed.</returns>
public bool TryConsume(int itemID, int count)
{
return this.TryConsume(item => item.Sample.ParentSheetIndex == itemID, count);
}
/****
** TryPush
****/
/// <summary>Add the given item stack to the pipes if there's space.</summary>
/// <param name="item">The item stack to push.</param>
public bool TryPush(ITrackedStack item)
{
if (item == null || item.Count <= 0)
return false;
int originalCount = item.Count;
var preferOutputContainers = this.Containers.Where(p => p.AllowsInput() && p.PreferForOutput());
var otherContainers = this.Containers.Where(p => p.AllowsInput() && !p.PreferForOutput());
// push into 'output' chests
foreach (IContainer container in preferOutputContainers)
{
container.Store(item);
if (item.Count <= 0)
return true;
}
// push into chests that already have this item
string itemKey = this.GetItemKey(item.Sample);
foreach (IContainer container in otherContainers)
{
if (container.All(p => this.GetItemKey(p.Sample) != itemKey))
continue;
container.Store(item);
if (item.Count <= 0)
return true;
}
// push into first available chest
if (item.Count >= 0)
{
foreach (IContainer container in otherContainers)
{
container.Store(item);
if (item.Count <= 0)
return true;
}
}
return item.Count < originalCount;
}
/*********
** Private methods
*********/
/// <summary>Get a key which uniquely identifies an item type.</summary>
/// <param name="item">The item to identify.</param>
private string GetItemKey(Item item)
{
string key = item.GetType().FullName;
if (item is SObject obj)
key += "_craftable:" + obj.bigCraftable.Value;
key += "_id:" + item.ParentSheetIndex;
return key;
}
}
}

View File

@ -0,0 +1,18 @@
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>An automatable entity, which can implement a more specific type like <see cref="IMachine"/> or <see cref="IContainer"/>. If it doesn't implement a more specific type, it's treated as a connector with no additional logic.</summary>
public interface IAutomatable
{
/*********
** Accessors
*********/
/// <summary>The location which contains the machine.</summary>
GameLocation Location { get; }
/// <summary>The tile area covered by the machine.</summary>
Rectangle TileArea { get; }
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>The API which lets other mods interact with Automate.</summary>
public interface IAutomateAPI
{
/// <summary>Add an automation factory.</summary>
/// <param name="factory">An automation factory which construct machines, containers, and connectors.</param>
void AddFactory(IAutomationFactory factory);
/// <summary>Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
IDictionary<Vector2, int> GetMachineStates(GameLocation location, Rectangle tileArea);
}
}

View File

@ -0,0 +1,43 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.TerrainFeatures;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Automate
{
/// <summary>Constructs machines, containers, or connectors which can be added to a machine group.</summary>
public interface IAutomationFactory
{
/*********
** Accessors
*********/
/// <summary>Get a machine, container, or connector instance for a given object.</summary>
/// <param name="obj">The in-game object.</param>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
IAutomatable GetFor(SObject obj, GameLocation location, in Vector2 tile);
/// <summary>Get a machine, container, or connector instance for a given terrain feature.</summary>
/// <param name="feature">The terrain feature.</param>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
IAutomatable GetFor(TerrainFeature feature, GameLocation location, in Vector2 tile);
/// <summary>Get a machine, container, or connector instance for a given building.</summary>
/// <param name="building">The building.</param>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
IAutomatable GetFor(Building building, BuildableGameLocation location, in Vector2 tile);
/// <summary>Get a machine, container, or connector instance for a given tile position.</summary>
/// <param name="location">The location to check.</param>
/// <param name="tile">The tile position to check.</param>
/// <returns>Returns an instance or <c>null</c>.</returns>
IAutomatable GetForTile(GameLocation location, in Vector2 tile);
}
}

View File

@ -0,0 +1,34 @@
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>An ingredient stack (or stacks) which can be consumed by a machine.</summary>
public interface IConsumable
{
/*********
** Accessors
*********/
/// <summary>The items available to consumable.</summary>
ITrackedStack Consumables { get; }
/// <summary>A sample item for comparison.</summary>
/// <remarks>This should not be a reference to the original stack.</remarks>
Item Sample { get; }
/// <summary>The number of items needed for the recipe.</summary>
int CountNeeded { get; }
/// <summary>Whether the consumables needed for this requirement are ready.</summary>
bool IsMet { get; }
/*********
** Public methods
*********/
/// <summary>Remove the needed number of this item from the stack.</summary>
void Reduce();
/// <summary>Remove the needed number of this item from the stack and return a new stack matching the count.</summary>
Item Take();
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>Provides and stores items for machines.</summary>
public interface IContainer : IAutomatable, IEnumerable<ITrackedStack>
{
/*********
** Accessors
*********/
/// <summary>The container name (if any).</summary>
string Name { get; }
/*********
** Public methods
*********/
/// <summary>Find items in the pipe matching a predicate.</summary>
/// <param name="predicate">Matches items that should be returned.</param>
/// <param name="count">The number of items to find.</param>
/// <returns>If the pipe has no matching item, returns <c>null</c>. Otherwise returns a tracked item stack, which may have less items than requested if no more were found.</returns>
ITrackedStack Get(Func<Item, bool> predicate, int count);
/// <summary>Store an item stack.</summary>
/// <param name="stack">The item stack to store.</param>
/// <remarks>If the storage can't hold the entire stack, it should reduce the tracked stack accordingly.</remarks>
void Store(ITrackedStack stack);
}
}

View File

@ -0,0 +1,28 @@
namespace Pathoschild.Stardew.Automate
{
/// <summary>A machine that accepts input and provides output.</summary>
public interface IMachine : IAutomatable
{
/*********
** Accessors
*********/
/// <summary>A unique ID for the machine type.</summary>
/// <remarks>This value should be identical for two machines if they have the exact same behavior and input logic. For example, if one machine in a group can't process input due to missing items, Automate will skip any other empty machines of that type in the same group since it assumes they need the same inputs.</remarks>
string MachineTypeID { get; }
/*********
** Public methods
*********/
/// <summary>Get the machine's processing state.</summary>
MachineState GetState();
/// <summary>Get the output item.</summary>
ITrackedStack GetOutput();
/// <summary>Provide input to the machine.</summary>
/// <param name="input">The available items.</param>
/// <returns>Returns whether the machine started processing an item.</returns>
bool SetInput(IStorage input);
}
}

View File

@ -0,0 +1,33 @@
using System;
using StardewValley;
using Object = StardewValley.Object;
namespace Pathoschild.Stardew.Automate
{
/// <summary>Describes a generic recipe based on item input and output.</summary>
public interface IRecipe
{
/*********
** Accessors
*********/
/// <summary>The input item or category ID.</summary>
int InputID { get; }
/// <summary>The number of inputs needed.</summary>
int InputCount { get; }
/// <summary>The output to generate (given an input).</summary>
Func<Item, Object> Output { get; }
/// <summary>The time needed to prepare an output.</summary>
int Minutes { get; }
/*********
** Methods
*********/
/// <summary>Get whether the recipe can accept a given item as input (regardless of stack size).</summary>
/// <param name="stack">The item to check.</param>
bool AcceptsInput(ITrackedStack stack);
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using Pathoschild.Stardew.Automate.Framework;
namespace Pathoschild.Stardew.Automate
{
/// <summary>Manages access to items in the underlying containers.</summary>
public interface IStorage
{
/*********
** Public methods
*********/
/// <summary>Get all items from the given pipes.</summary>
IEnumerable<ITrackedStack> GetItems();
/****
** TryGetIngredient
****/
/// <summary>Get an ingredient needed for a recipe.</summary>
/// <param name="predicate">Returns whether an item should be matched.</param>
/// <param name="count">The number of items to find.</param>
/// <param name="consumable">The matching consumables.</param>
/// <returns>Returns whether the requirement is met.</returns>
bool TryGetIngredient(Func<ITrackedStack, bool> predicate, int count, out IConsumable consumable);
/// <summary>Get an ingredient needed for a recipe.</summary>
/// <param name="id">The item or category ID.</param>
/// <param name="count">The number of items to find.</param>
/// <param name="consumable">The matching consumables.</param>
/// <returns>Returns whether the requirement is met.</returns>
bool TryGetIngredient(int id, int count, out IConsumable consumable);
/// <summary>Get an ingredient needed for a recipe.</summary>
/// <param name="recipes">The items to match.</param>
/// <param name="consumable">The matching consumables.</param>
/// <param name="recipe">The matched requisition.</param>
/// <returns>Returns whether the requirement is met.</returns>
bool TryGetIngredient(IRecipe[] recipes, out IConsumable consumable, out IRecipe recipe);
/****
** TryConsume
****/
/// <summary>Consume an ingredient needed for a recipe.</summary>
/// <param name="predicate">Returns whether an item should be matched.</param>
/// <param name="count">The number of items to find.</param>
/// <returns>Returns whether the item was consumed.</returns>
bool TryConsume(Func<ITrackedStack, bool> predicate, int count);
/// <summary>Consume an ingredient needed for a recipe.</summary>
/// <param name="itemID">The item ID.</param>
/// <param name="count">The number of items to find.</param>
/// <returns>Returns whether the item was consumed.</returns>
bool TryConsume(int itemID, int count);
/****
** TryPush
****/
/// <summary>Add the given item stack to the pipes if there's space.</summary>
/// <param name="item">The item stack to push.</param>
bool TryPush(ITrackedStack item);
}
}

View File

@ -0,0 +1,30 @@
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>An item stack in an input pipe which can be reduced or taken.</summary>
public interface ITrackedStack
{
/*********
** Accessors
*********/
/// <summary>A sample item for comparison.</summary>
/// <remarks>This should be equivalent to the underlying item (except in stack size), but *not* a reference to it.</remarks>
Item Sample { get; }
/// <summary>The number of items in the stack.</summary>
int Count { get; }
/*********
** Public methods
*********/
/// <summary>Remove the specified number of this item from the stack.</summary>
/// <param name="count">The number to consume.</param>
void Reduce(int count);
/// <summary>Remove the specified number of this item from the stack and return a new stack matching the count.</summary>
/// <param name="count">The number to get.</param>
Item Take(int count);
}
}

View File

@ -0,0 +1,18 @@
namespace Pathoschild.Stardew.Automate
{
/// <summary>A machine processing state.</summary>
public enum MachineState
{
/// <summary>The machine is not currently enabled (e.g. out of season or needs to be started manually).</summary>
Disabled,
/// <summary>The machine has no input.</summary>
Empty,
/// <summary>The machine is processing an input.</summary>
Processing,
/// <summary>The machine finished processing an input and has an output item ready.</summary>
Done
}
}

View File

@ -0,0 +1,285 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Pathoschild.Stardew.Automate.Framework;
using Pathoschild.Stardew.Automate.Framework.Models;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.Common.Utilities;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>The mod entry point.</summary>
internal class ModEntry : Mod
{
/*********
** Fields
*********/
/// <summary>The mod configuration.</summary>
private ModConfig Config;
/// <summary>Constructs machine groups.</summary>
private MachineGroupFactory Factory;
/// <summary>Whether to enable automation for the current save.</summary>
private bool EnableAutomation => Context.IsMainPlayer;
/// <summary>The machines to process.</summary>
private readonly IDictionary<GameLocation, MachineGroup[]> ActiveMachineGroups = new Dictionary<GameLocation, MachineGroup[]>(new ObjectReferenceComparer<GameLocation>());
/// <summary>The disabled machine groups (e.g. machines not connected to a chest).</summary>
private readonly IDictionary<GameLocation, MachineGroup[]> DisabledMachineGroups = new Dictionary<GameLocation, MachineGroup[]>(new ObjectReferenceComparer<GameLocation>());
/// <summary>The locations that should be reloaded on the next update tick.</summary>
private readonly HashSet<GameLocation> ReloadQueue = new HashSet<GameLocation>(new ObjectReferenceComparer<GameLocation>());
/// <summary>The number of ticks until the next automation cycle.</summary>
private int AutomateCountdown;
/// <summary>The current overlay being displayed, if any.</summary>
private OverlayMenu CurrentOverlay;
/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides methods for interacting with the mod directory, such as read/writing a config file or custom JSON files.</param>
public override void Entry(IModHelper helper)
{
// toggle mod compatibility
bool hasBetterJunimos = helper.ModRegistry.IsLoaded("hawkfalcon.BetterJunimos");
bool hasDeluxeAutoGrabber = helper.ModRegistry.IsLoaded("stokastic.DeluxeGrabber");
// init
this.Config = helper.ReadConfig<ModConfig>();
this.Factory = new MachineGroupFactory();
this.Factory.Add(new AutomationFactory(this.Config.Connectors, this.Config.AutomateShippingBin, this.Monitor, helper.Reflection, hasBetterJunimos, hasDeluxeAutoGrabber));
// hook events
helper.Events.GameLoop.SaveLoaded += this.OnSaveLoaded;
helper.Events.Player.Warped += this.OnWarped;
helper.Events.World.LocationListChanged += this.World_LocationListChanged;
helper.Events.World.ObjectListChanged += this.World_ObjectListChanged;
helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked;
helper.Events.Input.ButtonPressed += this.OnButtonPressed;
if (this.Config.Connectors.Any(p => p.Type == ObjectType.Floor))
helper.Events.World.TerrainFeatureListChanged += this.World_TerrainFeatureListChanged;
// log info
this.Monitor.VerboseLog($"Initialised with automation every {this.Config.AutomationInterval} ticks.");
}
/// <summary>Get an API that other mods can access. This is always called after <see cref="Entry" />.</summary>
public override object GetApi()
{
return new AutomateAPI(this.Monitor, this.Factory, this.ActiveMachineGroups, this.DisabledMachineGroups);
}
/*********
** Private methods
*********/
/****
** Event handlers
****/
/// <summary>The method invoked when the player loads a save.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
{
// disable if secondary player
if (!this.EnableAutomation)
this.Monitor.Log("Disabled automation (only the main player can automate machines in multiplayer mode).", LogLevel.Warn);
// reset
this.ActiveMachineGroups.Clear();
this.DisabledMachineGroups.Clear();
this.AutomateCountdown = this.Config.AutomationInterval;
this.DisableOverlay();
foreach (GameLocation location in CommonHelper.GetLocations())
this.ReloadQueue.Add(location);
}
/// <summary>The method invoked when the player warps to a new location.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnWarped(object sender, WarpedEventArgs e)
{
if (e.IsLocalPlayer)
this.ResetOverlayIfShown();
}
/// <summary>The method invoked when a location is added or removed.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void World_LocationListChanged(object sender, LocationListChangedEventArgs e)
{
if (!this.EnableAutomation)
return;
this.Monitor.VerboseLog("Location list changed, reloading all machines.");
try
{
this.ActiveMachineGroups.Clear();
this.DisabledMachineGroups.Clear();
foreach (GameLocation location in CommonHelper.GetLocations())
this.ReloadQueue.Add(location);
}
catch (Exception ex)
{
this.HandleError(ex, "updating locations");
}
}
/// <summary>The method invoked when an object is added or removed to a location.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void World_ObjectListChanged(object sender, ObjectListChangedEventArgs e)
{
if (!this.EnableAutomation)
return;
this.Monitor.VerboseLog($"Object list changed in {e.Location.Name}, reloading machines in current location.");
this.ReloadQueue.Add(e.Location);
}
/// <summary>The method invoked when a terrain feature is added or removed to a location.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void World_TerrainFeatureListChanged(object sender, TerrainFeatureListChangedEventArgs e)
{
if (!this.EnableAutomation)
return;
this.Monitor.VerboseLog($"Terrain feature list changed in {e.Location.Name}, reloading machines in current location.");
this.ReloadQueue.Add(e.Location);
}
/// <summary>The method invoked when the in-game clock time changes.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
{
if (!Context.IsWorldReady || !this.EnableAutomation)
return;
try
{
// handle delay
this.AutomateCountdown--;
if (this.AutomateCountdown > 0)
return;
this.AutomateCountdown = this.Config.AutomationInterval;
// reload machines if needed
if (this.ReloadQueue.Any())
{
foreach (GameLocation location in this.ReloadQueue)
this.ReloadMachinesIn(location);
this.ReloadQueue.Clear();
this.ResetOverlayIfShown();
}
// process machines
foreach (MachineGroup group in this.GetActiveMachineGroups())
group.Automate();
}
catch (Exception ex)
{
this.HandleError(ex, "processing machines");
}
}
/// <summary>The method invoked when the player presses a button.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
{
try
{
// toggle overlay
if (Context.IsPlayerFree && this.Config.Controls.ToggleOverlay.Contains(e.Button))
{
if (this.CurrentOverlay != null)
this.DisableOverlay();
else
this.EnableOverlay();
}
}
catch (Exception ex)
{
this.HandleError(ex, "handling key input");
}
}
/****
** Methods
****/
/// <summary>Get the active machine groups in every location.</summary>
private IEnumerable<MachineGroup> GetActiveMachineGroups()
{
foreach (KeyValuePair<GameLocation, MachineGroup[]> group in this.ActiveMachineGroups)
{
foreach (MachineGroup machineGroup in group.Value)
yield return machineGroup;
}
}
/// <summary>Reload the machines in a given location.</summary>
/// <param name="location">The location whose machines to reload.</param>
private void ReloadMachinesIn(GameLocation location)
{
this.Monitor.VerboseLog($"Reloading machines in {location.Name}...");
// get machine groups
MachineGroup[] machineGroups = this.Factory.GetMachineGroups(location).ToArray();
this.ActiveMachineGroups[location] = machineGroups.Where(p => p.HasInternalAutomation).ToArray();
this.DisabledMachineGroups[location] = machineGroups.Where(p => !p.HasInternalAutomation).ToArray();
// remove unneeded entries
if (!this.ActiveMachineGroups[location].Any())
this.ActiveMachineGroups.Remove(location);
if (!this.DisabledMachineGroups[location].Any())
this.DisabledMachineGroups.Remove(location);
}
/// <summary>Log an error and warn the user.</summary>
/// <param name="ex">The exception to handle.</param>
/// <param name="verb">The verb describing where the error occurred (e.g. "looking that up").</param>
private void HandleError(Exception ex, string verb)
{
this.Monitor.Log($"Something went wrong {verb}:\n{ex}", LogLevel.Error);
CommonHelper.ShowErrorMessage($"Huh. Something went wrong {verb}. The error log has the technical details.");
}
/// <summary>Disable the overlay, if shown.</summary>
private void DisableOverlay()
{
this.CurrentOverlay?.Dispose();
this.CurrentOverlay = null;
}
/// <summary>Enable the overlay.</summary>
private void EnableOverlay()
{
if (this.CurrentOverlay == null)
this.CurrentOverlay = new OverlayMenu(this.Helper.Events, this.Helper.Input, this.Factory.GetMachineGroups(Game1.currentLocation));
}
/// <summary>Reset the overlay if it's being shown.</summary>
private void ResetOverlayIfShown()
{
if (this.CurrentOverlay != null)
{
this.DisableOverlay();
this.EnableOverlay();
}
}
}
}

View File

@ -0,0 +1,102 @@
using System;
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>An item stack which notifies callbacks when it's reduced.</summary>
public class TrackedItem : ITrackedStack
{
/*********
** Fields
*********/
/// <summary>The item stack.</summary>
private readonly Item Item;
/// <summary>The callback invoked when the stack size is reduced (including reduced to zero).</summary>
protected readonly Action<Item> OnReduced;
/// <summary>The callback invoked when the stack is empty.</summary>
protected readonly Action<Item> OnEmpty;
/// <summary>The last stack size handlers were notified of.</summary>
private int LastStackSize;
/*********
** Accessors
*********/
/// <summary>A sample item for comparison.</summary>
/// <remarks>This should be equivalent to the underlying item (except in stack size), but *not* a reference to it.</remarks>
public Item Sample { get; }
/// <summary>The number of items in the stack.</summary>
public int Count => this.Item.Stack;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="item">The item stack.</param>
/// <param name="onReduced">The callback invoked when the stack size is reduced (including reduced to zero).</param>
/// <param name="onEmpty">The callback invoked when the stack is empty.</param>
public TrackedItem(Item item, Action<Item> onReduced = null, Action<Item> onEmpty = null)
{
this.Item = item ?? throw new InvalidOperationException("Can't track a null item stack.");
this.Sample = this.GetNewStack(item);
this.LastStackSize = item.Stack;
this.OnReduced = onReduced;
this.OnEmpty = onEmpty;
}
/// <summary>Remove the specified number of this item from the stack.</summary>
/// <param name="count">The number to consume.</param>
public void Reduce(int count)
{
this.Item.Stack -= Math.Max(0, count);
this.Delegate();
}
/// <summary>Remove the specified number of this item from the stack and return a new stack matching the count.</summary>
/// <param name="count">The number to get.</param>
public Item Take(int count)
{
if (count <= 0)
return null;
this.Reduce(count);
return this.GetNewStack(this.Item, count);
}
/*********
** Private methods
*********/
/// <summary>Notify handlers.</summary>
private void Delegate()
{
// skip if not reduced
if (this.Item.Stack >= this.LastStackSize)
return;
this.LastStackSize = this.Item.Stack;
// notify handlers
this.OnReduced?.Invoke(this.Item);
if (this.Item.Stack <= 0)
this.OnEmpty?.Invoke(this.Item);
}
/// <summary>Create a new stack of the given item.</summary>
/// <param name="original">The item stack to clone.</param>
/// <param name="stackSize">The new stack size.</param>
private Item GetNewStack(Item original, int stackSize = 1)
{
if (original == null)
return null;
Item stack = original.getOne();
stack.Stack = stackSize;
return stack;
}
}
}

View File

@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Linq;
using StardewValley;
namespace Pathoschild.Stardew.Automate
{
/// <summary>An item stack which wraps an underlying collection of stacks.</summary>
public class TrackedItemCollection : ITrackedStack
{
/*********
** Fields
*********/
/// <summary>The underlying item stacks.</summary>
private readonly ITrackedStack[] Stacks;
/*********
** Accessors
*********/
/// <summary>A sample item for comparison.</summary>
/// <remarks>This should be equivalent to the underlying item (except in stack size), but *not* a reference to it.</remarks>
public Item Sample { get; }
/// <summary>The number of items in the stack.</summary>
public int Count => this.Stacks.Sum(p => p.Count);
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="stacks">The underlying item stacks.</param>
public TrackedItemCollection(IEnumerable<ITrackedStack> stacks)
{
this.Stacks = stacks.ToArray();
this.Sample = this.Stacks.FirstOrDefault()?.Sample;
}
/// <summary>Remove the specified number of this item from the stack.</summary>
/// <param name="count">The number to consume.</param>
public void Reduce(int count)
{
if (count <= 0 || !this.Stacks.Any())
return;
// reduce
int left = count;
foreach (ITrackedStack stack in this.Stacks)
{
// skip, stack empty
if (stack.Count <= 0)
continue;
// take entire stack
if (stack.Count < left)
{
left -= stack.Count;
stack.Reduce(stack.Count);
continue;
}
// take remaining items
stack.Reduce(left);
break;
}
}
/// <summary>Remove the specified number of this item from the stack and return a new stack matching the count.</summary>
/// <param name="count">The number to get.</param>
public Item Take(int count)
{
if (count <= 0 || !this.Stacks.Any())
return null;
// reduce
this.Reduce(count);
// create new stack
Item item = this.Sample.getOne();
item.Stack = count;
return item;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,345 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common.UI;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Locations;
using StardewValley.Menus;
namespace Pathoschild.Stardew.Common
{
/// <summary>Provides common utility methods for interacting with the game code shared by my various mods.</summary>
internal static class CommonHelper
{
/*********
** Fields
*********/
/// <summary>A blank pixel which can be colorised and stretched to draw geometric shapes.</summary>
private static readonly Lazy<Texture2D> LazyPixel = new Lazy<Texture2D>(() =>
{
Texture2D pixel = new Texture2D(Game1.graphics.GraphicsDevice, 1, 1);
pixel.SetData(new[] { Color.White });
return pixel;
});
/*********
** Accessors
*********/
/// <summary>A blank pixel which can be colorised and stretched to draw geometric shapes.</summary>
public static Texture2D Pixel => CommonHelper.LazyPixel.Value;
/// <summary>The width of the horizontal and vertical scroll edges (between the origin position and start of content padding).</summary>
public static readonly Vector2 ScrollEdgeSize = new Vector2(CommonSprites.Scroll.TopLeft.Width * Game1.pixelZoom, CommonSprites.Scroll.TopLeft.Height * Game1.pixelZoom);
/*********
** Public methods
*********/
/****
** Game
****/
/// <summary>Get all game locations.</summary>
public static IEnumerable<GameLocation> GetLocations()
{
return Game1.locations
.Concat(
from location in Game1.locations.OfType<BuildableGameLocation>()
from building in location.buildings
where building.indoors.Value != null
select building.indoors.Value
);
}
/****
** Fonts
****/
/// <summary>Get the dimensions of a space character.</summary>
/// <param name="font">The font to measure.</param>
public static float GetSpaceWidth(SpriteFont font)
{
return font.MeasureString("A B").X - font.MeasureString("AB").X;
}
/****
** UI
****/
/// <summary>Draw a pretty hover box for the given text.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="label">The text to display.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The maximum width to display.</param>
public static Vector2 DrawHoverBox(SpriteBatch spriteBatch, string label, in Vector2 position, float wrapWidth)
{
const int paddingSize = 27;
const int gutterSize = 20;
Vector2 labelSize = spriteBatch.DrawTextBlock(Game1.smallFont, label, position + new Vector2(gutterSize), wrapWidth); // draw text to get wrapped text dimensions
IClickableMenu.drawTextureBox(spriteBatch, Game1.menuTexture, new Rectangle(0, 256, 60, 60), (int)position.X, (int)position.Y, (int)labelSize.X + paddingSize + gutterSize, (int)labelSize.Y + paddingSize, Color.White);
spriteBatch.DrawTextBlock(Game1.smallFont, label, position + new Vector2(gutterSize), wrapWidth); // draw again over texture box
return labelSize + new Vector2(paddingSize);
}
/// <summary>Draw a button background.</summary>
/// <param name="spriteBatch">The sprite batch to which to draw.</param>
/// <param name="position">The top-left pixel coordinate at which to draw the button.</param>
/// <param name="contentSize">The button content's pixel size.</param>
/// <param name="contentPos">The pixel position at which the content begins.</param>
/// <param name="bounds">The button's outer bounds.</param>
/// <param name="padding">The padding between the content and border.</param>
public static void DrawButton(SpriteBatch spriteBatch, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding = 0)
{
CommonHelper.DrawContentBox(
spriteBatch: spriteBatch,
texture: CommonSprites.Button.Sheet,
background: CommonSprites.Button.Background,
top: CommonSprites.Button.Top,
right: CommonSprites.Button.Right,
bottom: CommonSprites.Button.Bottom,
left: CommonSprites.Button.Left,
topLeft: CommonSprites.Button.TopLeft,
topRight: CommonSprites.Button.TopRight,
bottomRight: CommonSprites.Button.BottomRight,
bottomLeft: CommonSprites.Button.BottomLeft,
position: position,
contentSize: contentSize,
contentPos: out contentPos,
bounds: out bounds,
padding: padding
);
}
/// <summary>Draw a scroll background.</summary>
/// <param name="spriteBatch">The sprite batch to which to draw.</param>
/// <param name="position">The top-left pixel coordinate at which to draw the scroll.</param>
/// <param name="contentSize">The scroll content's pixel size.</param>
/// <param name="contentPos">The pixel position at which the content begins.</param>
/// <param name="bounds">The scroll's outer bounds.</param>
/// <param name="padding">The padding between the content and border.</param>
public static void DrawScroll(SpriteBatch spriteBatch, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding = 5)
{
CommonHelper.DrawContentBox(
spriteBatch: spriteBatch,
texture: CommonSprites.Scroll.Sheet,
background: in CommonSprites.Scroll.Background,
top: CommonSprites.Scroll.Top,
right: CommonSprites.Scroll.Right,
bottom: CommonSprites.Scroll.Bottom,
left: CommonSprites.Scroll.Left,
topLeft: CommonSprites.Scroll.TopLeft,
topRight: CommonSprites.Scroll.TopRight,
bottomRight: CommonSprites.Scroll.BottomRight,
bottomLeft: CommonSprites.Scroll.BottomLeft,
position: position,
contentSize: contentSize,
contentPos: out contentPos,
bounds: out bounds,
padding: padding
);
}
/// <summary>Draw a generic content box like a scroll or button.</summary>
/// <param name="spriteBatch">The sprite batch to which to draw.</param>
/// <param name="texture">The texture to draw.</param>
/// <param name="background">The source rectangle for the background.</param>
/// <param name="top">The source rectangle for the top border.</param>
/// <param name="right">The source rectangle for the right border.</param>
/// <param name="bottom">The source rectangle for the bottom border.</param>
/// <param name="left">The source rectangle for the left border.</param>
/// <param name="topLeft">The source rectangle for the top-left corner.</param>
/// <param name="topRight">The source rectangle for the top-right corner.</param>
/// <param name="bottomRight">The source rectangle for the bottom-right corner.</param>
/// <param name="bottomLeft">The source rectangle for the bottom-left corner.</param>
/// <param name="position">The top-left pixel coordinate at which to draw the button.</param>
/// <param name="contentSize">The button content's pixel size.</param>
/// <param name="contentPos">The pixel position at which the content begins.</param>
/// <param name="bounds">The box's outer bounds.</param>
/// <param name="padding">The padding between the content and border.</param>
public static void DrawContentBox(SpriteBatch spriteBatch, Texture2D texture, in Rectangle background, in Rectangle top, in Rectangle right, in Rectangle bottom, in Rectangle left, in Rectangle topLeft, in Rectangle topRight, in Rectangle bottomRight, in Rectangle bottomLeft, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding)
{
int cornerWidth = topLeft.Width * Game1.pixelZoom;
int cornerHeight = topLeft.Height * Game1.pixelZoom;
int innerWidth = (int)(contentSize.X + padding * 2);
int innerHeight = (int)(contentSize.Y + padding * 2);
int outerWidth = innerWidth + cornerWidth * 2;
int outerHeight = innerHeight + cornerHeight * 2;
int x = (int)position.X;
int y = (int)position.Y;
// draw scroll background
spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y + cornerHeight, innerWidth, innerHeight), background, Color.White);
// draw borders
spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y, innerWidth, cornerHeight), top, Color.White);
spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y + cornerHeight + innerHeight, innerWidth, cornerHeight), bottom, Color.White);
spriteBatch.Draw(texture, new Rectangle(x, y + cornerHeight, cornerWidth, innerHeight), left, Color.White);
spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y + cornerHeight, cornerWidth, innerHeight), right, Color.White);
// draw corners
spriteBatch.Draw(texture, new Rectangle(x, y, cornerWidth, cornerHeight), topLeft, Color.White);
spriteBatch.Draw(texture, new Rectangle(x, y + cornerHeight + innerHeight, cornerWidth, cornerHeight), bottomLeft, Color.White);
spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y, cornerWidth, cornerHeight), topRight, Color.White);
spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y + cornerHeight + innerHeight, cornerWidth, cornerHeight), bottomRight, Color.White);
// set out params
contentPos = new Vector2(x + cornerWidth + padding, y + cornerHeight + padding);
bounds = new Rectangle(x, y, outerWidth, outerHeight);
}
/// <summary>Show an informational message to the player.</summary>
/// <param name="message">The message to show.</param>
/// <param name="duration">The number of milliseconds during which to keep the message on the screen before it fades (or <c>null</c> for the default time).</param>
public static void ShowInfoMessage(string message, int? duration = null)
{
Game1.addHUDMessage(new HUDMessage(message, 3) { noIcon = true, timeLeft = duration ?? HUDMessage.defaultTime });
}
/// <summary>Show an error message to the player.</summary>
/// <param name="message">The message to show.</param>
public static void ShowErrorMessage(string message)
{
Game1.addHUDMessage(new HUDMessage(message, 3));
}
/****
** Drawing
****/
/// <summary>Draw a sprite to the screen.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="x">The X-position at which to start the line.</param>
/// <param name="y">The X-position at which to start the line.</param>
/// <param name="size">The line dimensions.</param>
/// <param name="color">The color to tint the sprite.</param>
public static void DrawLine(this SpriteBatch batch, float x, float y, in Vector2 size, in Color? color = null)
{
batch.Draw(CommonHelper.Pixel, new Rectangle((int)x, (int)y, (int)size.X, (int)size.Y), color ?? Color.White);
}
/// <summary>Draw a block of text to the screen with the specified wrap width.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="font">The sprite font.</param>
/// <param name="text">The block of text to write.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The width at which to wrap the text.</param>
/// <param name="color">The text color.</param>
/// <param name="bold">Whether to draw bold text.</param>
/// <param name="scale">The font scale.</param>
/// <returns>Returns the text dimensions.</returns>
public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, string text, in Vector2 position, float wrapWidth, in Color? color = null, bool bold = false, float scale = 1)
{
if (text == null)
return new Vector2(0, 0);
// get word list
List<string> words = new List<string>();
foreach (string word in text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
{
// split on newlines
string wordPart = word;
int newlineIndex;
while ((newlineIndex = wordPart.IndexOf(Environment.NewLine, StringComparison.InvariantCulture)) >= 0)
{
if (newlineIndex == 0)
{
words.Add(Environment.NewLine);
wordPart = wordPart.Substring(Environment.NewLine.Length);
}
else if (newlineIndex > 0)
{
words.Add(wordPart.Substring(0, newlineIndex));
words.Add(Environment.NewLine);
wordPart = wordPart.Substring(newlineIndex + Environment.NewLine.Length);
}
}
// add remaining word (after newline split)
if (wordPart.Length > 0)
words.Add(wordPart);
}
// track draw values
float xOffset = 0;
float yOffset = 0;
float lineHeight = font.MeasureString("ABC").Y * scale;
float spaceWidth = CommonHelper.GetSpaceWidth(font) * scale;
float blockWidth = 0;
float blockHeight = lineHeight;
foreach (string word in words)
{
// check wrap width
float wordWidth = font.MeasureString(word).X * scale;
if (word == Environment.NewLine || ((wordWidth + xOffset) > wrapWidth && (int)xOffset != 0))
{
xOffset = 0;
yOffset += lineHeight;
blockHeight += lineHeight;
}
if (word == Environment.NewLine)
continue;
// draw text
Vector2 wordPosition = new Vector2(position.X + xOffset, position.Y + yOffset);
if (bold)
Utility.drawBoldText(batch, word, font, wordPosition, color ?? Color.Black, scale);
else
batch.DrawString(font, word, wordPosition, color ?? Color.Black, 0, Vector2.Zero, scale, SpriteEffects.None, 1);
// update draw values
if (xOffset + wordWidth > blockWidth)
blockWidth = xOffset + wordWidth;
xOffset += wordWidth + spaceWidth;
}
// return text position & dimensions
return new Vector2(blockWidth, blockHeight);
}
/****
** Error handling
****/
/// <summary>Intercept errors thrown by the action.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="verb">The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font.</param>
/// <param name="action">The action to invoke.</param>
/// <param name="onError">A callback invoked if an error is intercepted.</param>
public static void InterceptErrors(this IMonitor monitor, string verb, Action action, Action<Exception> onError = null)
{
monitor.InterceptErrors(verb, null, action, onError);
}
/// <summary>Intercept errors thrown by the action.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="verb">The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font.</param>
/// <param name="detailedVerb">A more detailed form of <see cref="verb"/> if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font.</param>
/// <param name="action">The action to invoke.</param>
/// <param name="onError">A callback invoked if an error is intercepted.</param>
public static void InterceptErrors(this IMonitor monitor, string verb, string detailedVerb, Action action, Action<Exception> onError = null)
{
try
{
action();
}
catch (Exception ex)
{
monitor.InterceptError(ex, verb, detailedVerb);
onError?.Invoke(ex);
}
}
/// <summary>Log an error and warn the user.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="ex">The exception to handle.</param>
/// <param name="verb">The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font.</param>
/// <param name="detailedVerb">A more detailed form of <see cref="verb"/> if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font.</param>
public static void InterceptError(this IMonitor monitor, Exception ex, string verb, string detailedVerb = null)
{
detailedVerb = detailedVerb ?? verb;
monitor.Log($"Something went wrong {detailedVerb}:\n{ex}", LogLevel.Error);
CommonHelper.ShowErrorMessage($"Huh. Something went wrong {verb}. The error log has the technical details.");
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Linq;
using StardewModdingAPI.Utilities;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Common.DataParsers
{
/// <summary>Analyses crop data for a tile.</summary>
internal class CropDataParser
{
/*********
** Accessors
*********/
/// <summary>The crop.</summary>
public Crop Crop { get; }
/// <summary>The seasons in which the crop grows.</summary>
public string[] Seasons { get; }
/// <summary>The phase index in <see cref="StardewValley.Crop.phaseDays"/> when the crop can be harvested.</summary>
public int HarvestablePhase { get; }
/// <summary>The number of days needed between planting and first harvest.</summary>
public int DaysToFirstHarvest { get; }
/// <summary>The number of days needed between harvests, after the first harvest.</summary>
public int DaysToSubsequentHarvest { get; }
/// <summary>Whether the crop can be harvested multiple times.</summary>
public bool HasMultipleHarvests { get; }
/// <summary>Whether the crop is ready to harvest now.</summary>
public bool CanHarvestNow { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="crop">The crop.</param>
public CropDataParser(Crop crop)
{
this.Crop = crop;
if (crop != null)
{
this.Seasons = crop.seasonsToGrowIn.ToArray();
this.HasMultipleHarvests = crop.regrowAfterHarvest.Value == -1;
this.HarvestablePhase = crop.phaseDays.Count - 1;
this.CanHarvestNow = (crop.currentPhase.Value >= this.HarvestablePhase) && (!crop.fullyGrown.Value || crop.dayOfCurrentPhase.Value <= 0);
this.DaysToFirstHarvest = crop.phaseDays.Take(crop.phaseDays.Count - 1).Sum(); // ignore harvestable phase
this.DaysToSubsequentHarvest = crop.regrowAfterHarvest.Value;
}
}
/// <summary>Get the date when the crop will next be ready to harvest.</summary>
public SDate GetNextHarvest()
{
// get crop
Crop crop = this.Crop;
if (crop == null)
throw new InvalidOperationException("Can't get the harvest date because there's no crop.");
// ready now
if (this.CanHarvestNow)
return SDate.Now();
// growing: days until next harvest
if (!crop.fullyGrown.Value)
{
int daysUntilLastPhase = this.DaysToFirstHarvest - this.Crop.dayOfCurrentPhase.Value - crop.phaseDays.Take(crop.currentPhase.Value).Sum();
return SDate.Now().AddDays(daysUntilLastPhase);
}
// regrowable crop harvested today
if (crop.dayOfCurrentPhase.Value >= crop.regrowAfterHarvest.Value)
return SDate.Now().AddDays(crop.regrowAfterHarvest.Value);
// regrowable crop
// dayOfCurrentPhase decreases to 0 when fully grown, where <=0 is harvestable
return SDate.Now().AddDays(crop.dayOfCurrentPhase.Value);
}
/// <summary>Get a sample item acquired by harvesting the crop.</summary>
public Item GetSampleDrop()
{
if (this.Crop == null)
throw new InvalidOperationException("Can't get a sample drop because there's no crop.");
return new SObject(this.Crop.indexOfHarvest.Value, 1);
}
}
}

View File

@ -0,0 +1,44 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.Automate
{
/// <summary>Handles the logic for integrating with the Automate mod.</summary>
internal class AutomateIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IAutomateApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public AutomateIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Automate", "Pathoschild.Automate", "1.11.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IAutomateApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
public IDictionary<Vector2, int> GetMachineStates(GameLocation location, Rectangle tileArea)
{
this.AssertLoaded();
return this.ModApi.GetMachineStates(location, tileArea);
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.Automate
{
/// <summary>The API provided by the Automate mod.</summary>
public interface IAutomateApi
{
/// <summary>Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
IDictionary<Vector2, int> GetMachineStates(GameLocation location, Rectangle tileArea);
}
}

View File

@ -0,0 +1,82 @@
using System;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations
{
/// <summary>The base implementation for a mod integration.</summary>
internal abstract class BaseIntegration : IModIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's unique ID.</summary>
protected string ModID { get; }
/// <summary>An API for fetching metadata about loaded mods.</summary>
protected IModRegistry ModRegistry { get; }
/// <summary>Encapsulates monitoring and logging.</summary>
protected IMonitor Monitor { get; }
/*********
** Accessors
*********/
/// <summary>A human-readable name for the mod.</summary>
public string Label { get; }
/// <summary>Whether the mod is available.</summary>
public bool IsLoaded { get; protected set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="label">A human-readable name for the mod.</param>
/// <param name="modID">The mod's unique ID.</param>
/// <param name="minVersion">The minimum version of the mod that's supported.</param>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
protected BaseIntegration(string label, string modID, string minVersion, IModRegistry modRegistry, IMonitor monitor)
{
// init
this.Label = label;
this.ModID = modID;
this.ModRegistry = modRegistry;
this.Monitor = monitor;
// validate mod
IManifest manifest = modRegistry.Get(this.ModID)?.Manifest;
if (manifest == null)
return;
if (manifest.Version.IsOlderThan(minVersion))
{
monitor.Log($"Detected {label} {manifest.Version}, but need {minVersion} or later. Disabled integration with this mod.", LogLevel.Warn);
return;
}
this.IsLoaded = true;
}
/// <summary>Get an API for the mod, and show a message if it can't be loaded.</summary>
/// <typeparam name="TInterface">The API type.</typeparam>
protected TInterface GetValidatedApi<TInterface>() where TInterface : class
{
TInterface api = this.ModRegistry.GetApi<TInterface>(this.ModID);
if (api == null)
{
this.Monitor.Log($"Detected {this.Label}, but couldn't fetch its API. Disabled integration with this mod.", LogLevel.Warn);
return null;
}
return api;
}
/// <summary>Assert that the integration is loaded.</summary>
/// <exception cref="InvalidOperationException">The integration isn't loaded.</exception>
protected void AssertLoaded()
{
if (!this.IsLoaded)
throw new InvalidOperationException($"The {this.Label} integration isn't loaded.");
}
}
}

View File

@ -0,0 +1,40 @@
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.BetterJunimos
{
/// <summary>Handles the logic for integrating with the Better Junimos mod.</summary>
internal class BetterJunimosIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IBetterJunimosApi ModApi;
/*********
** Accessors
*********/
/// <summary>The Junimo Hut coverage radius.</summary>
public int MaxRadius { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public BetterJunimosIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Better Junimos", "hawkfalcon.BetterJunimos", "0.5.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IBetterJunimosApi>();
this.IsLoaded = this.ModApi != null;
this.MaxRadius = this.ModApi?.GetJunimoHutMaxRadius() ?? 0;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Pathoschild.Stardew.Common.Integrations.BetterJunimos
{
/// <summary>The API provided by the Better Junimos mod.</summary>
public interface IBetterJunimosApi
{
/// <summary>Get the maximum radius for Junimo Huts.</summary>
int GetJunimoHutMaxRadius();
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.BetterSprinklers
{
/// <summary>Handles the logic for integrating with the Better Sprinklers mod.</summary>
internal class BetterSprinklersIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IBetterSprinklersApi ModApi;
/*********
** Accessors
*********/
/// <summary>The maximum possible sprinkler radius.</summary>
public int MaxRadius { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public BetterSprinklersIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Better Sprinklers", "Speeder.BetterSprinklers", "2.3.1-unofficial.6-pathoschild", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IBetterSprinklersApi>();
this.IsLoaded = this.ModApi != null;
this.MaxRadius = this.ModApi?.GetMaxGridSize() ?? 0;
}
/// <summary>Get the configured Sprinkler tiles relative to (0, 0).</summary>
public IDictionary<int, Vector2[]> GetSprinklerTiles()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerCoverage();
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.BetterSprinklers
{
/// <summary>The API provided by the Better Sprinklers mod.</summary>
public interface IBetterSprinklersApi
{
/// <summary>Get the maximum supported coverage width or height.</summary>
int GetMaxGridSize();
/// <summary>Get the relative tile coverage by supported sprinkler ID.</summary>
IDictionary<int, Vector2[]> GetSprinklerCoverage();
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.Cobalt
{
/// <summary>Handles the logic for integrating with the Cobalt mod.</summary>
internal class CobaltIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly ICobaltApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public CobaltIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Cobalt", "spacechase0.Cobalt", "1.1", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<ICobaltApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the cobalt sprinkler's object ID.</summary>
public int GetSprinklerId()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerId();
}
/// <summary>Get the configured Sprinkler tiles relative to (0, 0).</summary>
public IEnumerable<Vector2> GetSprinklerTiles()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerCoverage(Vector2.Zero);
}
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.Cobalt
{
/// <summary>The API provided by the Cobalt mod.</summary>
public interface ICobaltApi
{
/*********
** Public methods
*********/
/// <summary>Get the cobalt sprinkler's object ID.</summary>
int GetSprinklerId();
/// <summary>Get the cobalt sprinkler coverage.</summary>
/// <param name="origin">The tile position containing the sprinkler.</param>
IEnumerable<Vector2> GetSprinklerCoverage(Vector2 origin);
}
}

View File

@ -0,0 +1,49 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Common.Integrations.CustomFarmingRedux
{
/// <summary>Handles the logic for integrating with the Custom Farming Redux mod.</summary>
internal class CustomFarmingReduxIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly ICustomFarmingApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public CustomFarmingReduxIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Custom Farming Redux", "Platonymous.CustomFarming", "2.8.5", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<ICustomFarmingApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the sprite info for a custom object, or <c>null</c> if the object isn't custom.</summary>
/// <param name="obj">The custom object.</param>
public SpriteInfo GetSprite(SObject obj)
{
this.AssertLoaded();
Tuple<Item, Texture2D, Rectangle, Color> data = this.ModApi.getRealItemAndTexture(obj);
return data != null
? new SpriteInfo(data.Item2, data.Item3)
: null;
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.CustomFarmingRedux
{
/// <summary>The API provided by the Custom Farming Redux mod.</summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "The naming convention is defined by the Custom Farming Redux mod.")]
public interface ICustomFarmingApi
{
/*********
** Public methods
*********/
/// <summary>Get metadata for a custom machine and draw metadata for an object.</summary>
/// <param name="dummy">The item that would be replaced by the custom item.</param>
Tuple<Item, Texture2D, Rectangle, Color> getRealItemAndTexture(StardewValley.Object dummy);
}
}

View File

@ -0,0 +1,49 @@
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.FarmExpansion
{
/// <summary>Handles the logic for integrating with the Farm Expansion mod.</summary>
internal class FarmExpansionIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IFarmExpansionApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public FarmExpansionIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Farm Expansion", "Advize.FarmExpansion", "3.3", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IFarmExpansionApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Add a blueprint to all future carpenter menus for the farm area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
public void AddFarmBluePrint(BluePrint blueprint)
{
this.AssertLoaded();
this.ModApi.AddFarmBluePrint(blueprint);
}
/// <summary>Add a blueprint to all future carpenter menus for the expansion area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
public void AddExpansionBluePrint(BluePrint blueprint)
{
this.AssertLoaded();
this.ModApi.AddExpansionBluePrint(blueprint);
}
}
}

View File

@ -0,0 +1,16 @@
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.FarmExpansion
{
/// <summary>The API provided by the Farm Expansion mod.</summary>
public interface IFarmExpansionApi
{
/// <summary>Add a blueprint to all future carpenter menus for the farm area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
void AddFarmBluePrint(BluePrint blueprint);
/// <summary>Add a blueprint to all future carpenter menus for the expansion area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
void AddExpansionBluePrint(BluePrint blueprint);
}
}

View File

@ -0,0 +1,15 @@
namespace Pathoschild.Stardew.Common.Integrations
{
/// <summary>Handles integration with a given mod.</summary>
internal interface IModIntegration
{
/*********
** Accessors
*********/
/// <summary>A human-readable name for the mod.</summary>
string Label { get; }
/// <summary>Whether the mod is available.</summary>
bool IsLoaded { get; }
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.LineSprinklers
{
/// <summary>The API provided by the Line Sprinklers mod.</summary>
public interface ILineSprinklersApi
{
/// <summary>Get the maximum supported coverage width or height.</summary>
int GetMaxGridSize();
/// <summary>Get the relative tile coverage by supported sprinkler ID.</summary>
IDictionary<int, Vector2[]> GetSprinklerCoverage();
}
}

Some files were not shown because too many files have changed in this diff Show More