diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..2f7c3c53 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,141 @@ +[*] +charset = utf-8-bom +end_of_line = crlf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +# Microsoft .NET properties +csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_elsewhere = false:none +csharp_style_var_for_built_in_types = false:suggestion +dotnet_naming_rule.property_rule.import_to_resharper = as_predefined +dotnet_naming_rule.property_rule.resharper_style = AaBb, Show + AaBb +dotnet_naming_rule.property_rule.severity = warning +dotnet_naming_rule.property_rule.style = upper_camel_case_style +dotnet_naming_rule.property_rule.symbols = property_symbols +dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule.severity = warning +dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols +dotnet_naming_rule.unity_serialized_field_rule_1.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule_1.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule_1.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule_1.severity = warning +dotnet_naming_rule.unity_serialized_field_rule_1.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1 +dotnet_naming_rule.unity_serialized_field_rule_2.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule_2.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule_2.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule_2.severity = warning +dotnet_naming_rule.unity_serialized_field_rule_2.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule_2.symbols = unity_serialized_field_symbols_2 +dotnet_naming_rule.unity_serialized_field_rule_3.import_to_resharper = True +dotnet_naming_rule.unity_serialized_field_rule_3.resharper_description = Unity serialized field +dotnet_naming_rule.unity_serialized_field_rule_3.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef +dotnet_naming_rule.unity_serialized_field_rule_3.severity = warning +dotnet_naming_rule.unity_serialized_field_rule_3.style = lower_camel_case_style +dotnet_naming_rule.unity_serialized_field_rule_3.symbols = unity_serialized_field_symbols_3 +dotnet_naming_style.lower_camel_case_style.capitalization = camel_case +dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case +dotnet_naming_symbols.property_symbols.applicable_accessibilities = * +dotnet_naming_symbols.property_symbols.applicable_kinds = property +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance +dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance +dotnet_naming_symbols.unity_serialized_field_symbols_2.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols_2.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols_2.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols_2.resharper_required_modifiers = instance +dotnet_naming_symbols.unity_serialized_field_symbols_3.applicable_accessibilities = * +dotnet_naming_symbols.unity_serialized_field_symbols_3.applicable_kinds = +dotnet_naming_symbols.unity_serialized_field_symbols_3.resharper_applicable_kinds = unity_serialised_field +dotnet_naming_symbols.unity_serialized_field_symbols_3.resharper_required_modifiers = instance +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_argument = true +resharper_align_multiline_calls_chain = true +resharper_align_multiline_extends_list = true +resharper_align_multiline_parameter = true +resharper_align_multiple_declaration = true +resharper_align_multline_type_parameter_constrains = true +resharper_align_multline_type_parameter_list = true +resharper_align_tuple_components = true +resharper_autodetect_indent_settings = true +resharper_braces_for_for = required +resharper_braces_for_foreach = required +resharper_braces_for_ifelse = required +resharper_braces_for_while = required +resharper_csharp_blank_lines_inside_region = 0 +resharper_csharp_empty_block_style = together_same_line +resharper_csharp_int_align_comments = true +resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_after_invocation_lpar = true +resharper_csharp_wrap_arguments_style = chop_if_long +resharper_csharp_wrap_before_declaration_rpar = true +resharper_csharp_wrap_before_invocation_rpar = true +resharper_csharp_wrap_parameters_style = chop_if_long +resharper_enforce_line_ending_style = true +resharper_formatter_off_tag = @formatter:off +resharper_formatter_on_tag = @formatter:on +resharper_formatter_tags_enabled = true +resharper_for_built_in_types = use_var_when_evident +resharper_indent_preprocessor_region = outdent +resharper_keep_existing_embedded_arrangement = false +resharper_keep_existing_expr_member_arrangement = false +resharper_keep_existing_initializer_arrangement = false +resharper_keep_existing_linebreaks = false +resharper_outdent_statement_labels = true +resharper_place_simple_embedded_statement_on_same_line = false +resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_if_long +resharper_wrap_before_first_method_call = false +resharper_wrap_before_primary_constructor_declaration_rpar = true +resharper_wrap_chained_binary_expressions = chop_if_long +resharper_wrap_chained_binary_patterns = chop_if_long +resharper_wrap_chained_method_calls = chop_if_long +resharper_wrap_list_pattern = chop_if_long + +# ReSharper inspection severities +resharper_arrange_redundant_parentheses_highlighting = hint +resharper_arrange_this_qualifier_highlighting = hint +resharper_arrange_type_member_modifiers_highlighting = hint +resharper_arrange_type_modifiers_highlighting = hint +resharper_built_in_type_reference_style_for_member_access_highlighting = hint +resharper_built_in_type_reference_style_highlighting = hint +resharper_redundant_base_qualifier_highlighting = warning +resharper_suggest_var_or_type_built_in_types_highlighting = hint +resharper_suggest_var_or_type_elsewhere_highlighting = hint +resharper_suggest_var_or_type_simple_types_highlighting = hint +resharper_web_config_module_not_resolved_highlighting = warning +resharper_web_config_type_not_resolved_highlighting = warning +resharper_web_config_wrong_module_highlighting = warning + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_style = space +indent_size = 2 + +[*.{appxmanifest,asax,ascx,aspx,axaml,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,hh,hlsl,hlsli,hlslinc,hpp,hxx,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,skin,tpp,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}] +indent_style = space +indent_size = 2 +tab_width = 2 diff --git a/UIInfoSuite2.sln.DotSettings b/UIInfoSuite2.sln.DotSettings new file mode 100644 index 00000000..8e565a9e --- /dev/null +++ b/UIInfoSuite2.sln.DotSettings @@ -0,0 +1,20 @@ + + True + True + True + True + True + True + True + True + True + 0 + TOGETHER_SAME_LINE + OUTDENT + True + True + CHOP_IF_LONG + True + True + True + True \ No newline at end of file diff --git a/UIInfoSuite2/AdditionalFeatures/SkipIntro.cs b/UIInfoSuite2/AdditionalFeatures/SkipIntro.cs index 12aaa16d..f0987e30 100644 --- a/UIInfoSuite2/AdditionalFeatures/SkipIntro.cs +++ b/UIInfoSuite2/AdditionalFeatures/SkipIntro.cs @@ -6,31 +6,32 @@ namespace UIInfoSuite2.AdditionalFeatures { - public class SkipIntro - { - private readonly IModEvents _events; + public class SkipIntro + { + private readonly IModEvents _events; - public SkipIntro(IModEvents events) - { - _events = events; + public SkipIntro(IModEvents events) + { + _events = events; - events.Input.ButtonPressed += OnButtonPressed; - events.GameLoop.SaveLoaded += OnSaveLoaded; - } + events.Input.ButtonPressed += OnButtonPressed; + events.GameLoop.SaveLoaded += OnSaveLoaded; + } - private void OnSaveLoaded(object sender, EventArgs e) - { - _events.Input.ButtonPressed -= OnButtonPressed; - _events.GameLoop.SaveLoaded -= OnSaveLoaded; - } + private void OnSaveLoaded(object sender, EventArgs e) + { + _events.Input.ButtonPressed -= OnButtonPressed; + _events.GameLoop.SaveLoaded -= OnSaveLoaded; + } - private void OnButtonPressed(object sender, ButtonPressedEventArgs e) - { - if (Game1.activeClickableMenu is TitleMenu menu && (e.Button == SButton.Escape || e.Button == SButton.ControllerStart)) - { - menu.skipToTitleButtons(); - _events.Input.ButtonPressed -= OnButtonPressed; - } - } + private void OnButtonPressed(object sender, ButtonPressedEventArgs e) + { + if (Game1.activeClickableMenu is TitleMenu menu && + (e.Button == SButton.Escape || e.Button == SButton.ControllerStart)) + { + menu.skipToTitleButtons(); + _events.Input.ButtonPressed -= OnButtonPressed; + } } + } } diff --git a/UIInfoSuite2/Compatibility/DynamicGameAssetHelper.cs b/UIInfoSuite2/Compatibility/DynamicGameAssetHelper.cs index 2eec7f88..b603ecf9 100644 --- a/UIInfoSuite2/Compatibility/DynamicGameAssetHelper.cs +++ b/UIInfoSuite2/Compatibility/DynamicGameAssetHelper.cs @@ -3,273 +3,321 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using StardewValley; +using Netcode; using StardewModdingAPI; using StardewModdingAPI.Events; +using StardewValley; +using StardewValley.Menus; +using StardewValley.Network; using UIInfoSuite2.Infrastructure.Reflection; using SObject = StardewValley.Object; namespace UIInfoSuite2.Compatibility { - public class DynamicGameAssetsHelper + public class DynamicGameAssetsHelper + { + private Assembly? _dgaAssembly; + + private DgaFakeIdRetriever? _dgaFakeId; + + private IReflectedMethod? _modFindMethod; + + public DynamicGameAssetsHelper(IDynamicGameAssetsApi api, IModHelper helper, IMonitor monitor) { - public IDynamicGameAssetsApi Api { get; init; } - private IReflectionHelper Reflection { get; init; } - private IModEvents ModEvents { get; init; } - private IMonitor Monitor { get; init; } - private Reflector Reflector { get; init; } + Api = api; + Reflection = helper.Reflection; + ModEvents = helper.Events; + Monitor = monitor; + Reflector = new Reflector(); - private Assembly? _dgaAssembly; + ModEvents.GameLoop.DayEnding += OnDayEnding; + } - private DgaFakeIdRetriever? _dgaFakeId; - private DgaFakeIdRetriever DgaFakeId { - get => _dgaFakeId ??= new DgaFakeIdRetriever(this); - } + public IDynamicGameAssetsApi Api { get; init; } + private IReflectionHelper Reflection { get; } + private IModEvents ModEvents { get; } + private IMonitor Monitor { get; } + private Reflector Reflector { get; } - private IReflectedMethod? _modFindMethod; + private DgaFakeIdRetriever DgaFakeId => _dgaFakeId ??= new DgaFakeIdRetriever(this); - public DynamicGameAssetsHelper(IDynamicGameAssetsApi api, IModHelper helper, IMonitor monitor) - { - this.Api = api; - this.Reflection = helper.Reflection; - this.ModEvents = helper.Events; - this.Monitor = monitor; - this.Reflector = new Reflector(); + /// Inject an object of any DGA type to get a reference to the DGA Assembly + /// The initialized . + public DynamicGameAssetsHelper InjectDga(object dga) + { + if (_dgaAssembly == null) + { + _dgaAssembly = dga.GetType().Assembly; + Monitor.Log( + $"{GetType().Name}: Retrieved reference to DGA assemby using DGA class instance of {dga.GetType().FullName}." + ); + } + + return this; + } - this.ModEvents.GameLoop.DayEnding += OnDayEnding; - } + public void Dispose() + { + ModEvents.GameLoop.DayEnding -= OnDayEnding; + } - /// Inject an object of any DGA type to get a reference to the DGA Assembly - /// The initialized . - public DynamicGameAssetsHelper InjectDga(object dga) + private void OnDayEnding(object? sender, DayEndingEventArgs e) + { + Reflector.NewCacheInterval(); + } + + /// Mod.Find() + public object? FindPackData(string fullId) + { + IReflectedMethod? modFind = GetModFindMethod(); + if (modFind == null) + { + throw new Exception("Could not load DynamicGameAssets.Mod.Find"); + } + + return modFind.Invoke(fullId); + } + + public string GetDgaObjectFakeId(SObject dgaItem) + { + return dgaItem.ItemId; + } + + #region Code loading from DGA assembly + private IReflectedMethod? GetModFindMethod() + { + if (_modFindMethod != null) + { + return _modFindMethod; + } + + if (_dgaAssembly == null) + { + return null; + } + + Type modClass = _dgaAssembly.GetType("DynamicGameAssets.Mod")!; + _modFindMethod = Reflection.GetMethod(modClass, "Find"); + return _modFindMethod; + } + #endregion + + /// Retrieve fake object ids for DGA object using code copy-pasted from DGA. + /// But it first checks using a roundabout way, that the copy-pasted code is still valid. + private class DgaFakeIdRetriever + { + public DgaFakeIdRetriever(DynamicGameAssetsHelper dgaHelper) + { + DgaHelper = dgaHelper; + } + + private DynamicGameAssetsHelper DgaHelper { get; } + + public string GetId(SObject dgaItem) + { + // TODO 1.6 -- I'm assuming this deterministic hash code thing was some shenanigans to deal with not having a unique ID + return dgaItem.ItemId; + //if (deterministicHashCodeIsCorrect == null) + //{ + // int hashedId = this.GetIdByDeterministicHashCode(dgaItem); + // string shippedId = this.GetIdByShippingIt(dgaItem); + // deterministicHashCodeIsCorrect = (hashedId == shippedId); + + // if ((bool) deterministicHashCodeIsCorrect) + // DgaHelper.Monitor.Log($"{this.GetType().Name}: The GetDeterministicHashCode implementation seems to be correct", LogLevel.Trace); + // else + // DgaHelper.Monitor.Log($"{this.GetType().Name}: The GetDeterministicHashCode implementation seems to be incorrect. Processing DGA items will be slower.", LogLevel.Info); + + // return shippedId; + //} + //else if (deterministicHashCodeIsCorrect == true) + //{ + // return this.GetIdByDeterministicHashCode(dgaItem); + //} + //else + //{ + // return this.GetIdByShippingIt(dgaItem); + //} + } + + private string GetIdByDeterministicHashCode(SObject dgaItem) + { + // TODO 1.6 -- I'm assuming this deterministic hash code thing was some shenanigans to deal with not having a unique ID + return dgaItem.ItemId; + //return this.GetDeterministicHashCode(DgaHelper.GetFullId(dgaItem)!); + } + + private string GetIdByShippingIt(SObject dgaItem) + { + DgaHelper.Monitor.Log($"{GetType().Name}: Retrieving the fake DGA item ID for {dgaItem.Name} by shipping it."); + + var shippingMenu = new ShippingMenu(new List()); + + // Record previous state + uint oldCropsShipped = Game1.stats.CropsShipped; + var oldBasicShipped = new Dictionary( + Game1.player.basicShipped.FieldDict.Select(x => KeyValuePair.Create(x.Key, x.Value.Value)) + ); + + // Ship the item to observe side-effects + shippingMenu.parseItems(new List { dgaItem }); + + // Restore previous state + Game1.stats.CropsShipped = oldCropsShipped; + NetStringDictionary? basicShipped = Game1.player.basicShipped; + + // Find the new item + List newItems = new(); + foreach (string? shipped in basicShipped.Keys) { - if (_dgaAssembly == null) + if (oldBasicShipped.TryGetValue(shipped, out int oldValue)) + { + if (oldValue != basicShipped[shipped]) { - _dgaAssembly = dga.GetType().Assembly; - Monitor.Log($"{this.GetType().Name}: Retrieved reference to DGA assemby using DGA class instance of {dga.GetType().FullName}.", LogLevel.Trace); + basicShipped[shipped] = oldValue; + newItems.Add(shipped); } - return this; + } + else + { + basicShipped.Remove(shipped); + newItems.Add(shipped); + } } - public void Dispose() + if (newItems.Count != 1) { - this.ModEvents.GameLoop.DayEnding -= this.OnDayEnding; + throw new Exception($"{newItems.Count} items were shipped when we expected only one"); } - private void OnDayEnding(object? sender, DayEndingEventArgs e) - { - this.Reflector.NewCacheInterval(); - } + return newItems[0]; + } - /// Retrieve fake object ids for DGA object using code copy-pasted from DGA. - /// But it first checks using a roundabout way, that the copy-pasted code is still valid. - private class DgaFakeIdRetriever + // Copied from SpaceShared.CommonExtensions + private int GetDeterministicHashCode(string str) + { + unchecked { - private DynamicGameAssetsHelper DgaHelper { get; init; } - private bool? deterministicHashCodeIsCorrect = null; - - public DgaFakeIdRetriever(DynamicGameAssetsHelper dgaHelper) - { - this.DgaHelper = dgaHelper; - } - - public int GetId(SObject dgaItem) - { - if (deterministicHashCodeIsCorrect == null) - { - int hashedId = this.GetIdByDeterministicHashCode(dgaItem); - int shippedId = this.GetIdByShippingIt(dgaItem); - deterministicHashCodeIsCorrect = (hashedId == shippedId); - - if ((bool) deterministicHashCodeIsCorrect) - DgaHelper.Monitor.Log($"{this.GetType().Name}: The GetDeterministicHashCode implementation seems to be correct", LogLevel.Trace); - else - DgaHelper.Monitor.Log($"{this.GetType().Name}: The GetDeterministicHashCode implementation seems to be incorrect. Processing DGA items will be slower.", LogLevel.Info); - - return shippedId; - } - else if (deterministicHashCodeIsCorrect == true) - { - return this.GetIdByDeterministicHashCode(dgaItem); - } - else - { - return this.GetIdByShippingIt(dgaItem); - } - } + int hash1 = (5381 << 16) + 5381; + int hash2 = hash1; - private int GetIdByDeterministicHashCode(SObject dgaItem) + for (var i = 0; i < str.Length; i += 2) + { + hash1 = ((hash1 << 5) + hash1) ^ str[i]; + if (i == str.Length - 1) { - return this.GetDeterministicHashCode(DgaHelper.GetFullId(dgaItem)!); + break; } - private int GetIdByShippingIt(SObject dgaItem) - { - DgaHelper.Monitor.Log($"{this.GetType().Name}: Retrieving the fake DGA item ID for {dgaItem.Name} by shipping it.", LogLevel.Trace); - - var shippingMenu = new StardewValley.Menus.ShippingMenu(new List()); - - // Record previous state - uint oldCropsShipped = Game1.stats.CropsShipped; - var oldBasicShipped = new Dictionary(Game1.player.basicShipped.FieldDict.Select(x => KeyValuePair.Create(x.Key, x.Value.Value))); - - // Ship the item to observe side-effects - shippingMenu.parseItems(new List{ dgaItem }); - - // Restore previous state - Game1.stats.CropsShipped = oldCropsShipped; - var basicShipped = Game1.player.basicShipped; - - // Find the new item - List newItems = new(); - foreach (var shipped in basicShipped.Keys) - { - if (oldBasicShipped.TryGetValue(shipped, out int oldValue)) - { - if (oldValue != basicShipped[shipped]) - { - basicShipped[shipped] = oldValue; - newItems.Add(shipped); - } - } - else - { - basicShipped.Remove(shipped); - newItems.Add(shipped); - } - } - if (newItems.Count != 1) - throw new Exception($"{newItems.Count} items were shipped when we expected only one"); - - return newItems[0]; - } - - // Copied from SpaceShared.CommonExtensions - private int GetDeterministicHashCode(string str) - { - unchecked - { - int hash1 = (5381 << 16) + 5381; - int hash2 = hash1; - - for (int i = 0; i < str.Length; i += 2) - { - hash1 = ((hash1 << 5) + hash1) ^ str[i]; - if (i == str.Length - 1) - break; - hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; - } - - return hash1 + (hash2 * 1566083941); - } - } - } - - /// Mod.Find() - public object? FindPackData(string fullId) - { - var modFind = GetModFindMethod(); - if (modFind == null) - throw new Exception("Could not load DynamicGameAssets.Mod.Find"); + hash2 = ((hash2 << 5) + hash2) ^ str[i + 1]; + } - return modFind.Invoke(fullId); + return hash1 + hash2 * 1566083941; } + } + } - public int GetDgaObjectFakeId(SObject dgaItem) - { - return DgaFakeId.GetId(dgaItem); - } + #region DGA instance fields, methods and properties + /// CustomCrop.Data + private object? GetCropData(object customCrop) + { + return Reflector.GetPropertyGetter(customCrop, "Data").GetValue(); + } - #region DGA instance fields, methods and properties - /// CustomCrop.Data - private object? GetCropData(object customCrop) - { - return Reflector.GetPropertyGetter(customCrop, "Data").GetValue(); - } + /// IDGAItem.FullId + public string? GetFullId(object dgaItem) + { + return Reflection.GetProperty(dgaItem, "FullId").GetValue(); + } + #endregion - /// IDGAItem.FullId - public string? GetFullId(object dgaItem) - { - return Reflection.GetProperty(dgaItem, "FullId").GetValue(); - } - #endregion + #region Code reflecting into DGA + public SObject? GetCropHarvest(object customCrop) + { + object? cropData = GetCropData(customCrop); + if (cropData == null) + { + return null; + } - #region Code reflecting into DGA - public SObject? GetCropHarvest(object customCrop) - { - var cropData = this.GetCropData(customCrop); - if (cropData == null) - return null; - - return this.GetCropPackHarvest(cropData); - } + return GetCropPackHarvest(cropData); + } - public SObject? GetSeedsHarvest(Item item) - { - if (!(item is StardewValley.Object seedsObject && seedsObject.Category == StardewValley.Object.SeedsCategory)) - return null; - - var itemData = Reflector.GetPropertyGetter(item, "Data").GetValue(); - if (itemData == null) - return null; - - string? itemPlants = Reflection.GetProperty(itemData, "Plants").GetValue(); - if (itemPlants == null) - return null; - - var cropData = this.FindPackData(itemPlants); - if (cropData == null) - return null; - - return this.GetCropPackHarvest(cropData); - } + public SObject? GetSeedsHarvest(Item item) + { + if (!(item is SObject seedsObject && seedsObject.Category == SObject.SeedsCategory)) + { + return null; + } + + object? itemData = Reflector.GetPropertyGetter(item, "Data").GetValue(); + if (itemData == null) + { + return null; + } + + string? itemPlants = Reflection.GetProperty(itemData, "Plants").GetValue(); + if (itemPlants == null) + { + return null; + } + + object? cropData = FindPackData(itemPlants); + if (cropData == null) + { + return null; + } + + return GetCropPackHarvest(cropData); + } - private SObject? GetCropPackHarvest(object cropData) + private SObject? GetCropPackHarvest(object cropData) + { + IList cropPhases = Reflector.GetPropertyGetter(cropData, "Phases").GetValue(); + + // Find the last phase that has a harvest drop + IList? harvestDrops = null; + foreach (object? phase in cropPhases) + { + IList phaseDrops = Reflector.GetPropertyGetter(phase!, "HarvestedDrops").GetValue()!; + if (phaseDrops.Count > 0) { - var cropPhases = Reflector.GetPropertyGetter(cropData, "Phases").GetValue(); - - // Find the last phase that has a harvest drop - IList? harvestDrops = null; - foreach (var phase in cropPhases) - { - var phaseDrops = Reflector.GetPropertyGetter(phase!, "HarvestedDrops").GetValue()!; - if (phaseDrops.Count > 0) - harvestDrops = phaseDrops; - } - if (harvestDrops == null) - return null; - if (harvestDrops.Count > 1) - throw new Exception("DGA crops with multiple drops on the last harvest are not supported"); - - var possibleDrops = Reflector.GetPropertyGetter(harvestDrops[0]!, "Item").GetValue(); - if (possibleDrops.Count != 1) - throw new Exception("DGA crops with random drops are not supported"); - - var dropItem = Reflector.GetPropertyGetter(possibleDrops[0]!, "Value").GetValue()!; - string dropItemType = Reflector.GetPropertyGetter(dropItem, "Type").GetValue()!.ToString()!; - string dropItemValue = Reflector.GetPropertyGetter(dropItem, "Value").GetValue()!; - - if (dropItemType == "DGAItem") - return (StardewValley.Object) this.Api.SpawnDGAItem(dropItemValue); - else if (dropItemType == "VanillaItem") - return new StardewValley.Object(int.Parse(dropItemValue), 1); - else - throw new Exception("Harvest types other than DGAItem and VanillaItem are not supported"); - } - #endregion - - #region Code loading from DGA assembly - private IReflectedMethod? GetModFindMethod() { - if (_modFindMethod != null) - return _modFindMethod; - - if (_dgaAssembly == null) - return null; - - var modClass = _dgaAssembly.GetType("DynamicGameAssets.Mod")!; - _modFindMethod = Reflection.GetMethod(modClass, "Find"); - return _modFindMethod; + harvestDrops = phaseDrops; } - #endregion + } + + if (harvestDrops == null) + { + return null; + } + + if (harvestDrops.Count > 1) + { + throw new Exception("DGA crops with multiple drops on the last harvest are not supported"); + } + + IList possibleDrops = Reflector.GetPropertyGetter(harvestDrops[0]!, "Item").GetValue(); + if (possibleDrops.Count != 1) + { + throw new Exception("DGA crops with random drops are not supported"); + } + + object dropItem = Reflector.GetPropertyGetter(possibleDrops[0]!, "Value").GetValue()!; + var dropItemType = Reflector.GetPropertyGetter(dropItem, "Type").GetValue()!.ToString()!; + string dropItemValue = Reflector.GetPropertyGetter(dropItem, "Value").GetValue()!; + + if (dropItemType == "DGAItem") + { + return (SObject)Api.SpawnDGAItem(dropItemValue); + } + + if (dropItemType == "VanillaItem") + { + return new SObject(dropItemValue, 1); + } + + throw new Exception("Harvest types other than DGAItem and VanillaItem are not supported"); } + #endregion + } } diff --git a/UIInfoSuite2/Compatibility/DynamicGameAssetsEntry.cs b/UIInfoSuite2/Compatibility/DynamicGameAssetsEntry.cs index a10d53bb..e3495326 100644 --- a/UIInfoSuite2/Compatibility/DynamicGameAssetsEntry.cs +++ b/UIInfoSuite2/Compatibility/DynamicGameAssetsEntry.cs @@ -1,72 +1,80 @@ - -using StardewModdingAPI; -using StardewModdingAPI.Events; using System; using System.Diagnostics.CodeAnalysis; +using StardewModdingAPI; +using StardewModdingAPI.Events; namespace UIInfoSuite2.Compatibility { - /// Entrypoint for all things DGA - public class DynamicGameAssetsEntry : IDisposable + /// Entrypoint for all things DGA + public class DynamicGameAssetsEntry : IDisposable + { + private const string MOD_ID = "spacechase0.DynamicGameAssets"; + private DynamicGameAssetsHelper? _dgaHelper; + + public DynamicGameAssetsEntry(IModHelper helper, IMonitor monitor) { - private const string MOD_ID = "spacechase0.DynamicGameAssets"; + Helper = helper; + Monitor = monitor; - private IModHelper Helper { get; init; } - private IMonitor Monitor { get; init; } + Helper.Events.GameLoop.GameLaunched += OnGameLaunched; + } - public IDynamicGameAssetsApi? Api { get; private set; } - private DynamicGameAssetsHelper? _dgaHelper; + private IModHelper Helper { get; } + private IMonitor Monitor { get; } - public bool IsLoaded { get; private set; } + public IDynamicGameAssetsApi? Api { get; private set; } - public DynamicGameAssetsEntry(IModHelper helper, IMonitor monitor) - { - this.Helper = helper; - this.Monitor = monitor; + public bool IsLoaded { get; private set; } - this.Helper.Events.GameLoop.GameLaunched += OnGameLaunched; - } + public void Dispose() + { + Helper.Events.GameLoop.GameLaunched -= OnGameLaunched; + } - public void Dispose() - { - this.Helper.Events.GameLoop.GameLaunched -= OnGameLaunched; - } + private void OnGameLaunched(object? sender, GameLaunchedEventArgs e) + { + // Check if DGA is loaded + if (Helper.ModRegistry.IsLoaded(MOD_ID)) + { + IsLoaded = true; - private void OnGameLaunched(object? sender, GameLaunchedEventArgs e) + // Get DGA's API + var api = Helper.ModRegistry.GetApi(MOD_ID); + if (api != null) { - // Check if DGA is loaded - if (Helper.ModRegistry.IsLoaded(MOD_ID)) - { - this.IsLoaded = true; + Api = api; + _dgaHelper = new DynamicGameAssetsHelper(Api, Helper, Monitor); + } + } + } - // Get DGA's API - var api = Helper.ModRegistry.GetApi(MOD_ID); - if (api != null) - { - this.Api = api; - this._dgaHelper = new DynamicGameAssetsHelper(Api, Helper, Monitor); - } - } + /// + /// Check if is a DGA CustomCrop and provide a + /// + public bool IsCustomCrop(object obj, [NotNullWhen(true)] out DynamicGameAssetsHelper? dgaHelper) + { + dgaHelper = null; + if (IsLoaded && obj.GetType().FullName == "DynamicGameAssets.Game.CustomCrop") + { + dgaHelper = _dgaHelper?.InjectDga(obj); + } - } + return dgaHelper != null; + } - /// Check if is a DGA CustomCrop and provide a - public bool IsCustomCrop(object obj, [NotNullWhen(true)] out DynamicGameAssetsHelper? dgaHelper) - { - dgaHelper = null; - if (this.IsLoaded && obj.GetType().FullName == "DynamicGameAssets.Game.CustomCrop") - dgaHelper = _dgaHelper?.InjectDga(obj); - return dgaHelper != null; - } + /// + /// Check if is a DGA CustomObject and provide a + /// + public bool IsCustomObject(object obj, [NotNullWhen(true)] out DynamicGameAssetsHelper? dgaHelper) + { + dgaHelper = null; + if (IsLoaded && obj.GetType().FullName == "DynamicGameAssets.Game.CustomObject") + { + dgaHelper = _dgaHelper?.InjectDga(obj); + } - /// Check if is a DGA CustomObject and provide a - public bool IsCustomObject(object obj, [NotNullWhen(true)] out DynamicGameAssetsHelper? dgaHelper) - { - dgaHelper = null; - if (this.IsLoaded && obj.GetType().FullName == "DynamicGameAssets.Game.CustomObject") - dgaHelper = _dgaHelper?.InjectDga(obj); - return dgaHelper != null; - } + return dgaHelper != null; } + } } diff --git a/UIInfoSuite2/Compatibility/ExperienceBar.cs b/UIInfoSuite2/Compatibility/ExperienceBar.cs index 6b9150d1..4be50cc1 100644 --- a/UIInfoSuite2/Compatibility/ExperienceBar.cs +++ b/UIInfoSuite2/Compatibility/ExperienceBar.cs @@ -1,119 +1,126 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; using StardewValley.Menus; -using System; // ReSharper disable once CheckNamespace namespace UIInfoSuite2.UIElements { - // This part of the class is only user for compatibility reasons - public partial class ExperienceBar + // This part of the class is only user for compatibility reasons + public partial class ExperienceBar + { + // For providing compatibility for mods that patch this method + // TODO: When refactoring is done, mark this as deprecated and provide a long-term new method + // Notable dependents: MARGO -- Modular Gameplay Overhaul + private void DrawExperienceBar( + int barWidth, + int experienceGainedThisLevel, + int experienceRequiredForNextLevel, + int currentLevel + ) { - // For providing compatibility for mods that patch this method - // TODO: When refactoring is done, mark this as deprecated and provide a long-term new method - // Notable dependents: MARGO -- Modular Gameplay Overhaul - private void DrawExperienceBar(int barWidth, int experienceGainedThisLevel, int experienceRequiredForNextLevel, int currentLevel) - { - float leftSide = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Left; + float leftSide = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Left; - if (Game1.isOutdoorMapSmallerThanViewport()) - { - int num3 = Game1.currentLocation.map.Layers[0].LayerWidth * Game1.tileSize; - leftSide += (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right - num3) / 2; - } + if (Game1.isOutdoorMapSmallerThanViewport()) + { + int num3 = Game1.currentLocation.map.Layers[0].LayerWidth * Game1.tileSize; + leftSide += (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right - num3) / 2; + } - Game1.drawDialogueBox( - (int)leftSide, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 160, - 240, - 160, - false, - true); + Game1.drawDialogueBox( + (int)leftSide, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 160, + 240, + 160, + false, + true + ); - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, - barWidth, - 31), - _experienceFillColor.Value); + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, + barWidth, + 31 + ), + _experienceFillColor.Value + ); - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, - Math.Min(4, barWidth), - 31), - _experienceFillColor.Value); + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, + Math.Min(4, barWidth), + 31 + ), + _experienceFillColor.Value + ); - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, - barWidth, - 4), - _experienceFillColor.Value); + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, + barWidth, + 4 + ), + _experienceFillColor.Value + ); - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 36, - barWidth, - 4), - _experienceFillColor.Value); + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 36, + barWidth, + 4 + ), + _experienceFillColor.Value + ); - ClickableTextureComponent textureComponent = - new ClickableTextureComponent( - "", - new Rectangle( - (int)leftSide - 36, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 80, - 260, - 100), - "", - "", - Game1.mouseCursors, - new Rectangle(0, 0, 0, 0), - Game1.pixelZoom); + var textureComponent = new ClickableTextureComponent( + "", + new Rectangle((int)leftSide - 36, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 80, 260, 100), + "", + "", + Game1.mouseCursors, + new Rectangle(0, 0, 0, 0), + Game1.pixelZoom + ); - if (textureComponent.containsPoint(Game1.getMouseX(), Game1.getMouseY())) - { - Game1.drawWithBorder( - experienceGainedThisLevel + "/" + experienceRequiredForNextLevel, - Color.Black, - Color.Black, - new Vector2( - leftSide + 33, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70)); - } - else - { - Game1.spriteBatch.Draw( - Game1.mouseCursors, - new Vector2( - leftSide + 54, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 62), - _levelUpIconRectangle.Value, - _experienceFillColor.Value, - 0, - Vector2.Zero, - 2.9f, - SpriteEffects.None, - 0.85f); + if (textureComponent.containsPoint(Game1.getMouseX(), Game1.getMouseY())) + { + Game1.drawWithBorder( + experienceGainedThisLevel + "/" + experienceRequiredForNextLevel, + Color.Black, + Color.Black, + new Vector2(leftSide + 33, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70) + ); + } + else + { + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(leftSide + 54, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 62), + _levelUpIconRectangle.Value, + _experienceFillColor.Value, + 0, + Vector2.Zero, + 2.9f, + SpriteEffects.None, + 0.85f + ); - Game1.drawWithBorder( - currentLevel.ToString(), - Color.Black * 0.6f, - Color.Black, - new Vector2( - leftSide + 33, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70)); - } - } + Game1.drawWithBorder( + currentLevel.ToString(), + Color.Black * 0.6f, + Color.Black, + new Vector2(leftSide + 33, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70) + ); + } } -} \ No newline at end of file + } +} diff --git a/UIInfoSuite2/Compatibility/IDynamicGameAssetsApi.cs b/UIInfoSuite2/Compatibility/IDynamicGameAssetsApi.cs index 1a9d6106..ca6b2fa5 100644 --- a/UIInfoSuite2/Compatibility/IDynamicGameAssetsApi.cs +++ b/UIInfoSuite2/Compatibility/IDynamicGameAssetsApi.cs @@ -1,7 +1,7 @@ namespace UIInfoSuite2.Compatibility { - public interface IDynamicGameAssetsApi - { - public object SpawnDGAItem(string fullId); - } + public interface IDynamicGameAssetsApi + { + public object SpawnDGAItem(string fullId); + } } diff --git a/UIInfoSuite2/Compatibility/IGenericModConfigMenuApi.cs b/UIInfoSuite2/Compatibility/IGenericModConfigMenuApi.cs index 9fba2971..e44e97af 100644 --- a/UIInfoSuite2/Compatibility/IGenericModConfigMenuApi.cs +++ b/UIInfoSuite2/Compatibility/IGenericModConfigMenuApi.cs @@ -7,171 +7,325 @@ namespace UIInfoSuite2.Compatibility { - /// The API which lets other mods add a config UI through Generic Mod Config Menu. - public interface IGenericModConfigMenuApi - { - /********* - ** Methods - *********/ - /**** - ** Must be called first - ****/ - /// Register a mod whose config can be edited through the UI. - /// The mod's manifest. - /// Reset the mod's config to its default values. - /// Save the mod's current config to the config.json file. - /// Whether the options can only be edited from the title screen. - /// Each mod can only be registered once, unless it's deleted via before calling this again. - void Register(IManifest mod, Action reset, Action save, bool titleScreenOnly = false); - - - /**** - ** Basic options - ****/ - /// Add a section title at the current position in the form. - /// The mod's manifest. - /// The title text shown in the form. - /// The tooltip text shown when the cursor hovers on the title, or null to disable the tooltip. - void AddSectionTitle(IManifest mod, Func text, Func tooltip = null); - - /// Add a paragraph of text at the current position in the form. - /// The mod's manifest. - /// The paragraph text to display. - void AddParagraph(IManifest mod, Func text); - - /// Add an image at the current position in the form. - /// The mod's manifest. - /// The image texture to display. - /// The pixel area within the texture to display, or null to show the entire image. - /// The zoom factor to apply to the image. - void AddImage(IManifest mod, Func texture, Rectangle? texturePixelArea = null, int scale = Game1.pixelZoom); - - /// Add a boolean option at the current position in the form. - /// The mod's manifest. - /// Get the current value from the mod config. - /// Set a new value in the mod config. - /// The label text to show in the form. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - void AddBoolOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string fieldId = null); - - /// Add an integer option at the current position in the form. - /// The mod's manifest. - /// Get the current value from the mod config. - /// Set a new value in the mod config. - /// The label text to show in the form. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// The minimum allowed value, or null to allow any. - /// The maximum allowed value, or null to allow any. - /// The interval of values that can be selected. - /// Get the display text to show for a value, or null to show the number as-is. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - void AddNumberOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, int? min = null, int? max = null, int? interval = null, Func formatValue = null, string fieldId = null); - - /// Add a float option at the current position in the form. - /// The mod's manifest. - /// Get the current value from the mod config. - /// Set a new value in the mod config. - /// The label text to show in the form. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// The minimum allowed value, or null to allow any. - /// The maximum allowed value, or null to allow any. - /// The interval of values that can be selected. - /// Get the display text to show for a value, or null to show the number as-is. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - void AddNumberOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, float? min = null, float? max = null, float? interval = null, Func formatValue = null, string fieldId = null); - - /// Add a string option at the current position in the form. - /// The mod's manifest. - /// Get the current value from the mod config. - /// Set a new value in the mod config. - /// The label text to show in the form. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// The values that can be selected, or null to allow any. - /// Get the display text to show for a value from , or null to show the values as-is. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - void AddTextOption(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string[] allowedValues = null, Func formatAllowedValue = null, string fieldId = null); - - /// Add a key binding at the current position in the form. - /// The mod's manifest. - /// Get the current value from the mod config. - /// Set a new value in the mod config. - /// The label text to show in the form. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - void AddKeybind(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string fieldId = null); - - /// Add a key binding list at the current position in the form. - /// The mod's manifest. - /// Get the current value from the mod config. - /// Set a new value in the mod config. - /// The label text to show in the form. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - void AddKeybindList(IManifest mod, Func getValue, Action setValue, Func name, Func tooltip = null, string fieldId = null); - - - /**** - ** Multi-page management - ****/ - /// Start a new page in the mod's config UI, or switch to that page if it already exists. All options registered after this will be part of that page. - /// The mod's manifest. - /// The unique page ID. - /// The page title shown in its UI, or null to show the value. - /// You must also call to make the page accessible. This is only needed to set up a multi-page config UI. If you don't call this method, all options will be part of the mod's main config UI instead. - void AddPage(IManifest mod, string pageId, Func pageTitle = null); - - /// Add a link to a page added via at the current position in the form. - /// The mod's manifest. - /// The unique ID of the page to open when the link is clicked. - /// The link text shown in the form. - /// The tooltip text shown when the cursor hovers on the link, or null to disable the tooltip. - void AddPageLink(IManifest mod, string pageId, Func text, Func tooltip = null); - - - /**** - ** Advanced - ****/ - /// Add an option at the current position in the form using custom rendering logic. - /// The mod's manifest. - /// The label text to show in the form. - /// Draw the option in the config UI. This is called with the sprite batch being rendered and the pixel position at which to start drawing. - /// The tooltip text shown when the cursor hovers on the field, or null to disable the tooltip. - /// A callback raised just before the menu containing this option is opened. - /// A callback raised before the form's current values are saved to the config (i.e. before the save callback passed to ). - /// A callback raised after the form's current values are saved to the config (i.e. after the save callback passed to ). - /// A callback raised before the form is reset to its default values (i.e. before the reset callback passed to ). - /// A callback raised after the form is reset to its default values (i.e. after the reset callback passed to ). - /// A callback raised just before the menu containing this option is closed. - /// The pixel height to allocate for the option in the form, or null for a standard input-sized option. This is called and cached each time the form is opened. - /// The unique field ID for use with , or null to auto-generate a randomized ID. - /// The custom logic represented by the callback parameters is responsible for managing its own state if needed. For example, you can store state in a static field or use closures to use a state variable. - void AddComplexOption(IManifest mod, Func name, Action draw, Func tooltip = null, Action beforeMenuOpened = null, Action beforeSave = null, Action afterSave = null, Action beforeReset = null, Action afterReset = null, Action beforeMenuClosed = null, Func height = null, string fieldId = null); - - /// Set whether the options registered after this point can only be edited from the title screen. - /// The mod's manifest. - /// Whether the options can only be edited from the title screen. - /// This lets you have different values per-field. Most mods should just set it once in . - void SetTitleScreenOnlyForNextOptions(IManifest mod, bool titleScreenOnly); - - /// Register a method to notify when any option registered by this mod is edited through the config UI. - /// The mod's manifest. - /// The method to call with the option's unique field ID and new value. - /// Options use a randomized ID by default; you'll likely want to specify the fieldId argument when adding options if you use this. - void OnFieldChanged(IManifest mod, Action onChange); - - /// Open the config UI for a specific mod. - /// The mod's manifest. - void OpenModMenu(IManifest mod); - - /// Get the currently-displayed mod config menu, if any. - /// The manifest of the mod whose config menu is being shown, or null if not applicable. - /// The page ID being shown for the current config menu, or null if not applicable. This may be null even if a mod config menu is shown (e.g. because the mod doesn't have pages). - /// Returns whether a mod config menu is being shown. - bool TryGetCurrentMenu(out IManifest mod, out string page); - - /// Remove a mod from the config UI and delete all its options and pages. - /// The mod's manifest. - void Unregister(IManifest mod); - } -} \ No newline at end of file + /// The API which lets other mods add a config UI through Generic Mod Config Menu. + public interface IGenericModConfigMenuApi + { + /********* + ** Methods + *********/ + /**** + ** Must be called first + ****/ + /// Register a mod whose config can be edited through the UI. + /// The mod's manifest. + /// Reset the mod's config to its default values. + /// Save the mod's current config to the config.json file. + /// Whether the options can only be edited from the title screen. + /// + /// Each mod can only be registered once, unless it's deleted via before calling this + /// again. + /// + void Register(IManifest mod, Action reset, Action save, bool titleScreenOnly = false); + + + /**** + ** Basic options + ****/ + /// Add a section title at the current position in the form. + /// The mod's manifest. + /// The title text shown in the form. + /// + /// The tooltip text shown when the cursor hovers on the title, or null to disable the + /// tooltip. + /// + void AddSectionTitle(IManifest mod, Func text, Func tooltip = null); + + /// Add a paragraph of text at the current position in the form. + /// The mod's manifest. + /// The paragraph text to display. + void AddParagraph(IManifest mod, Func text); + + /// Add an image at the current position in the form. + /// The mod's manifest. + /// The image texture to display. + /// The pixel area within the texture to display, or null to show the entire image. + /// The zoom factor to apply to the image. + void AddImage( + IManifest mod, + Func texture, + Rectangle? texturePixelArea = null, + int scale = Game1.pixelZoom + ); + + /// Add a boolean option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + void AddBoolOption( + IManifest mod, + Func getValue, + Action setValue, + Func name, + Func tooltip = null, + string fieldId = null + ); + + /// Add an integer option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// The minimum allowed value, or null to allow any. + /// The maximum allowed value, or null to allow any. + /// The interval of values that can be selected. + /// Get the display text to show for a value, or null to show the number as-is. + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + void AddNumberOption( + IManifest mod, + Func getValue, + Action setValue, + Func name, + Func tooltip = null, + int? min = null, + int? max = null, + int? interval = null, + Func formatValue = null, + string fieldId = null + ); + + /// Add a float option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// The minimum allowed value, or null to allow any. + /// The maximum allowed value, or null to allow any. + /// The interval of values that can be selected. + /// Get the display text to show for a value, or null to show the number as-is. + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + void AddNumberOption( + IManifest mod, + Func getValue, + Action setValue, + Func name, + Func tooltip = null, + float? min = null, + float? max = null, + float? interval = null, + Func formatValue = null, + string fieldId = null + ); + + /// Add a string option at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// The values that can be selected, or null to allow any. + /// + /// Get the display text to show for a value from , or + /// null to show the values as-is. + /// + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + void AddTextOption( + IManifest mod, + Func getValue, + Action setValue, + Func name, + Func tooltip = null, + string[] allowedValues = null, + Func formatAllowedValue = null, + string fieldId = null + ); + + /// Add a key binding at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + void AddKeybind( + IManifest mod, + Func getValue, + Action setValue, + Func name, + Func tooltip = null, + string fieldId = null + ); + + /// Add a key binding list at the current position in the form. + /// The mod's manifest. + /// Get the current value from the mod config. + /// Set a new value in the mod config. + /// The label text to show in the form. + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + void AddKeybindList( + IManifest mod, + Func getValue, + Action setValue, + Func name, + Func tooltip = null, + string fieldId = null + ); + + + /**** + ** Multi-page management + ****/ + /// + /// Start a new page in the mod's config UI, or switch to that page if it already exists. All options registered + /// after this will be part of that page. + /// + /// The mod's manifest. + /// The unique page ID. + /// The page title shown in its UI, or null to show the value. + /// + /// You must also call to make the page accessible. This is only needed to set up a + /// multi-page config UI. If you don't call this method, all options will be part of the mod's main config UI instead. + /// + void AddPage(IManifest mod, string pageId, Func pageTitle = null); + + /// Add a link to a page added via at the current position in the form. + /// The mod's manifest. + /// The unique ID of the page to open when the link is clicked. + /// The link text shown in the form. + /// The tooltip text shown when the cursor hovers on the link, or null to disable the tooltip. + void AddPageLink(IManifest mod, string pageId, Func text, Func tooltip = null); + + + /**** + ** Advanced + ****/ + /// Add an option at the current position in the form using custom rendering logic. + /// The mod's manifest. + /// The label text to show in the form. + /// + /// Draw the option in the config UI. This is called with the sprite batch being rendered and the pixel + /// position at which to start drawing. + /// + /// + /// The tooltip text shown when the cursor hovers on the field, or null to disable the + /// tooltip. + /// + /// A callback raised just before the menu containing this option is opened. + /// + /// A callback raised before the form's current values are saved to the config (i.e. before the + /// save callback passed to ). + /// + /// + /// A callback raised after the form's current values are saved to the config (i.e. after the + /// save callback passed to ). + /// + /// + /// A callback raised before the form is reset to its default values (i.e. before the + /// reset callback passed to ). + /// + /// + /// A callback raised after the form is reset to its default values (i.e. after the reset + /// callback passed to ). + /// + /// A callback raised just before the menu containing this option is closed. + /// + /// The pixel height to allocate for the option in the form, or null for a standard input-sized + /// option. This is called and cached each time the form is opened. + /// + /// + /// The unique field ID for use with , or null to auto-generate a + /// randomized ID. + /// + /// + /// The custom logic represented by the callback parameters is responsible for managing its own state if needed. + /// For example, you can store state in a static field or use closures to use a state variable. + /// + void AddComplexOption( + IManifest mod, + Func name, + Action draw, + Func tooltip = null, + Action beforeMenuOpened = null, + Action beforeSave = null, + Action afterSave = null, + Action beforeReset = null, + Action afterReset = null, + Action beforeMenuClosed = null, + Func height = null, + string fieldId = null + ); + + /// Set whether the options registered after this point can only be edited from the title screen. + /// The mod's manifest. + /// Whether the options can only be edited from the title screen. + /// This lets you have different values per-field. Most mods should just set it once in . + void SetTitleScreenOnlyForNextOptions(IManifest mod, bool titleScreenOnly); + + /// Register a method to notify when any option registered by this mod is edited through the config UI. + /// The mod's manifest. + /// The method to call with the option's unique field ID and new value. + /// + /// Options use a randomized ID by default; you'll likely want to specify the fieldId argument when adding + /// options if you use this. + /// + void OnFieldChanged(IManifest mod, Action onChange); + + /// Open the config UI for a specific mod. + /// The mod's manifest. + void OpenModMenu(IManifest mod); + + /// Get the currently-displayed mod config menu, if any. + /// The manifest of the mod whose config menu is being shown, or null if not applicable. + /// + /// The page ID being shown for the current config menu, or null if not applicable. This may be + /// null even if a mod config menu is shown (e.g. because the mod doesn't have pages). + /// + /// Returns whether a mod config menu is being shown. + bool TryGetCurrentMenu(out IManifest mod, out string page); + + /// Remove a mod from the config UI and delete all its options and pages. + /// The mod's manifest. + void Unregister(IManifest mod); + } +} diff --git a/UIInfoSuite2/Compatibility/ILevelExtender.cs b/UIInfoSuite2/Compatibility/ILevelExtender.cs index db67ce0f..762ff853 100644 --- a/UIInfoSuite2/Compatibility/ILevelExtender.cs +++ b/UIInfoSuite2/Compatibility/ILevelExtender.cs @@ -1,8 +1,8 @@ namespace UIInfoSuite2.Compatibility { - public interface ILevelExtender - { - int[] CurrentXP(); - int[] RequiredXP(); - } + public interface ILevelExtender + { + int[] CurrentXP(); + int[] RequiredXP(); + } } diff --git a/UIInfoSuite2/Infrastructure/Extensions/CollectionExtensions.cs b/UIInfoSuite2/Infrastructure/Extensions/CollectionExtensions.cs index a0febee3..e9860f93 100644 --- a/UIInfoSuite2/Infrastructure/Extensions/CollectionExtensions.cs +++ b/UIInfoSuite2/Infrastructure/Extensions/CollectionExtensions.cs @@ -2,19 +2,25 @@ namespace UIInfoSuite2.Infrastructure.Extensions { - public static class CollectionExtensions + public static class CollectionExtensions + { + public static TValue SafeGet( + this IDictionary dictionary, + TKey key, + TValue defaultValue = default + ) { - public static TValue SafeGet(this IDictionary dictionary, TKey key, TValue defaultValue = default) - { - TValue value = defaultValue; - - if (dictionary != null) - { - if (!dictionary.TryGetValue(key, out value)) - value = defaultValue; - } + TValue value = defaultValue; - return value; + if (dictionary != null) + { + if (!dictionary.TryGetValue(key, out value)) + { + value = defaultValue; } + } + + return value; } + } } diff --git a/UIInfoSuite2/Infrastructure/Extensions/ObjectExtensions.cs b/UIInfoSuite2/Infrastructure/Extensions/ObjectExtensions.cs index 896026d5..4cbb3ca9 100644 --- a/UIInfoSuite2/Infrastructure/Extensions/ObjectExtensions.cs +++ b/UIInfoSuite2/Infrastructure/Extensions/ObjectExtensions.cs @@ -1,75 +1,76 @@ -using Microsoft.Xna.Framework; +using System.Collections.Generic; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewValley; -using System.Collections.Generic; namespace UIInfoSuite2.Infrastructure.Extensions { - public static class ObjectExtensions + public static class ObjectExtensions + { + #region Properties + private static readonly Dictionary NpcHeadShotSize = new() { - #region Properties - private static readonly Dictionary NpcHeadShotSize = new() - { - { "Piere", 9 }, - { "Sebastian", 7 }, - { "Evelyn", 5 }, - { "Penny", 6 }, - { "Jas", 6 }, - { "Caroline", 5 }, - { "Dwarf", 5 }, - { "Sam", 9 }, - { "Maru", 6 }, - { "Wizard", 9 }, - { "Jodi", 7 }, - { "Krobus", 7 }, - { "Alex", 8 }, - { "Kent", 10 }, - { "Linus", 4 }, - { "Harvey", 9 }, - { "Shane", 8 }, - { "Haley", 6 }, - { "Robin", 7 }, - { "Marlon", 2 }, - { "Emily", 8 }, - { "Marnie", 5 }, - { "Abigail", 7 }, - { "Leah", 6 }, - { "George", 5 }, - { "Elliott", 9 }, - { "Gus", 7 }, - { "Lewis", 8 }, - { "Demetrius", 11 }, - { "Pam", 5 }, - { "Vincent", 6 }, - { "Sandy", 7 }, - { "Clint", 10 }, - { "Willy", 10 } - }; - #endregion + { "Piere", 9 }, + { "Sebastian", 7 }, + { "Evelyn", 5 }, + { "Penny", 6 }, + { "Jas", 6 }, + { "Caroline", 5 }, + { "Dwarf", 5 }, + { "Sam", 9 }, + { "Maru", 6 }, + { "Wizard", 9 }, + { "Jodi", 7 }, + { "Krobus", 7 }, + { "Alex", 8 }, + { "Kent", 10 }, + { "Linus", 4 }, + { "Harvey", 9 }, + { "Shane", 8 }, + { "Haley", 6 }, + { "Robin", 7 }, + { "Marlon", 2 }, + { "Emily", 8 }, + { "Marnie", 5 }, + { "Abigail", 7 }, + { "Leah", 6 }, + { "George", 5 }, + { "Elliott", 9 }, + { "Gus", 7 }, + { "Lewis", 8 }, + { "Demetrius", 11 }, + { "Pam", 5 }, + { "Vincent", 6 }, + { "Sandy", 7 }, + { "Clint", 10 }, + { "Willy", 10 } + }; + #endregion - public static Rectangle GetHeadShot(this NPC npc) - { - int size; - if (!NpcHeadShotSize.TryGetValue(npc.Name, out size)) - size = 4; + public static Rectangle GetHeadShot(this NPC npc) + { + int size; + if (!NpcHeadShotSize.TryGetValue(npc.Name, out size)) + { + size = 4; + } - Rectangle mugShotSourceRect = npc.getMugShotSourceRect(); - mugShotSourceRect.Height -= size / 2; - mugShotSourceRect.Y -= size / 2; - return mugShotSourceRect; - } + Rectangle mugShotSourceRect = npc.getMugShotSourceRect(); + mugShotSourceRect.Height -= size / 2; + mugShotSourceRect.Y -= size / 2; + return mugShotSourceRect; + } - public static string SafeGetString(this IModHelper helper, string key) - { - var result = string.Empty; + public static string SafeGetString(this IModHelper helper, string key) + { + var result = string.Empty; - if (!string.IsNullOrEmpty(key) && - helper != null) - { - result = helper.Translation.Get(key); - } + if (!string.IsNullOrEmpty(key) && helper != null) + { + result = helper.Translation.Get(key); + } - return result; - } + return result; } + } } diff --git a/UIInfoSuite2/Infrastructure/Extensions/StringExtensions.cs b/UIInfoSuite2/Infrastructure/Extensions/StringExtensions.cs index afb62355..3dee65cf 100644 --- a/UIInfoSuite2/Infrastructure/Extensions/StringExtensions.cs +++ b/UIInfoSuite2/Infrastructure/Extensions/StringExtensions.cs @@ -1,39 +1,41 @@ namespace UIInfoSuite2.Infrastructure.Extensions { - internal static class StringExtensions + internal static class StringExtensions + { + public static int SafeParseInt32(this string s) { - public static int SafeParseInt32(this string s) - { - int result = 0; + var result = 0; - if (!string.IsNullOrWhiteSpace(s)) - { - int.TryParse(s, out result); - } + if (!string.IsNullOrWhiteSpace(s)) + { + int.TryParse(s, out result); + } - return result; - } + return result; + } - public static int SafeParseInt64(this string s) - { - int result = 0; + public static int SafeParseInt64(this string s) + { + var result = 0; - if (!string.IsNullOrWhiteSpace(s)) - int.TryParse(s, out result); + if (!string.IsNullOrWhiteSpace(s)) + { + int.TryParse(s, out result); + } - return result; - } + return result; + } - public static bool SafeParseBool(this string s) - { - bool result = false; + public static bool SafeParseBool(this string s) + { + var result = false; - if (!string.IsNullOrWhiteSpace(s)) - { - bool.TryParse(s, out result); - } + if (!string.IsNullOrWhiteSpace(s)) + { + bool.TryParse(s, out result); + } - return result; - } + return result; } + } } diff --git a/UIInfoSuite2/Infrastructure/IconHandler.cs b/UIInfoSuite2/Infrastructure/IconHandler.cs index f8255d85..af616c54 100644 --- a/UIInfoSuite2/Infrastructure/IconHandler.cs +++ b/UIInfoSuite2/Infrastructure/IconHandler.cs @@ -1,37 +1,34 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using StardewModdingAPI.Utilities; using StardewValley; -using System; namespace UIInfoSuite2.Infrastructure { - public sealed class IconHandler - { - public static IconHandler Handler { get; } = new(); - - private readonly PerScreen _amountOfVisibleIcons = new(); + public sealed class IconHandler + { + private readonly PerScreen _amountOfVisibleIcons = new(); - private IconHandler() - { + private IconHandler() { } - } + public static IconHandler Handler { get; } = new(); - public Point GetNewIconPosition() - { - int yPos = Game1.options.zoomButtons ? 290 : 260; - int xPosition = Tools.GetWidthInPlayArea() - 70 - 48 * _amountOfVisibleIcons.Value; - if (Game1.player.questLog.Any() || Game1.player.team.specialOrders.Any()) - { - xPosition -= 65; - } - ++_amountOfVisibleIcons.Value; - return new Point(xPosition, yPos); - } + public Point GetNewIconPosition() + { + int yPos = Game1.options.zoomButtons ? 290 : 260; + int xPosition = Tools.GetWidthInPlayArea() - 70 - 48 * _amountOfVisibleIcons.Value; + if (Game1.player.questLog.Any() || Game1.player.team.specialOrders.Any()) + { + xPosition -= 65; + } - public void Reset(object sender, EventArgs e) - { - _amountOfVisibleIcons.Value = 0; - } + ++_amountOfVisibleIcons.Value; + return new Point(xPosition, yPos); + } + public void Reset(object sender, EventArgs e) + { + _amountOfVisibleIcons.Value = 0; } + } } diff --git a/UIInfoSuite2/Infrastructure/LanguageKeys.cs b/UIInfoSuite2/Infrastructure/LanguageKeys.cs index 0044d325..640bafea 100644 --- a/UIInfoSuite2/Infrastructure/LanguageKeys.cs +++ b/UIInfoSuite2/Infrastructure/LanguageKeys.cs @@ -1,38 +1,38 @@ namespace UIInfoSuite2.Infrastructure { - public static class LanguageKeys - { - public const string OptionsTabTooltip = "OptionsTabTooltip"; - public const string Days = "Days"; - public const string DaysToMature = "DaysToMature"; - public const string Hours = "Hours"; - public const string Minutes = "Minutes"; - public const string ReadyToHarvest = "ReadyToHarvest"; - public const string TodaysRecipe = "TodaysRecipe"; - public const string TravelingMerchantIsInTown = "TravelingMerchantIsInTown"; - public const string RainNextDay = "RainNextDay"; - public const string ThunderstormNextDay = "ThunderstormNextDay"; - public const string SnowNextDay = "SnowNextDay"; - public const string IslandRainNextDay = "IslandRainNextDay"; - public const string IslandThunderstormNextDay = "IslandThunderstormNextDay"; - public const string HarvestPrice = "HarvestPrice"; - public const string LevelUp = "LevelUp"; - public const string Calendar = "Calendar"; - public const string Billboard = "Billboard"; - public const string DaysUntilToolIsUpgraded = "DaysUntilToolIsUpgraded"; - public const string ToolIsFinishedBeingUpgraded = "ToolIsFinishedBeingUpgraded"; - public const string DailyLuckValue = "DailyLuckValue"; - public const string LuckStatus1 = "LuckStatus1"; - public const string LuckStatus2 = "LuckStatus2"; - public const string LuckStatus3 = "LuckStatus3"; - public const string LuckStatus4 = "LuckStatus4"; - public const string LuckStatus5 = "LuckStatus5"; - public const string LuckStatus6 = "LuckStatus6"; - public const string RobinBuildingStatus = "RobinBuildingStatus"; - public const string RobinHouseUpgradeStatus = "RobinHouseUpgradeStatus"; - public const string NpcBirthday = "NpcBirthday"; - public const string CanFindSalmonberry = "CanFindSalmonberry"; - public const string CanFindBlackberry = "CanFindBlackberry"; - public const string CanFindHazelnut = "CanFindHazelnut"; - } + public static class LanguageKeys + { + public const string OptionsTabTooltip = "OptionsTabTooltip"; + public const string Days = "Days"; + public const string DaysToMature = "DaysToMature"; + public const string Hours = "Hours"; + public const string Minutes = "Minutes"; + public const string ReadyToHarvest = "ReadyToHarvest"; + public const string TodaysRecipe = "TodaysRecipe"; + public const string TravelingMerchantIsInTown = "TravelingMerchantIsInTown"; + public const string RainNextDay = "RainNextDay"; + public const string ThunderstormNextDay = "ThunderstormNextDay"; + public const string SnowNextDay = "SnowNextDay"; + public const string IslandRainNextDay = "IslandRainNextDay"; + public const string IslandThunderstormNextDay = "IslandThunderstormNextDay"; + public const string HarvestPrice = "HarvestPrice"; + public const string LevelUp = "LevelUp"; + public const string Calendar = "Calendar"; + public const string Billboard = "Billboard"; + public const string DaysUntilToolIsUpgraded = "DaysUntilToolIsUpgraded"; + public const string ToolIsFinishedBeingUpgraded = "ToolIsFinishedBeingUpgraded"; + public const string DailyLuckValue = "DailyLuckValue"; + public const string LuckStatus1 = "LuckStatus1"; + public const string LuckStatus2 = "LuckStatus2"; + public const string LuckStatus3 = "LuckStatus3"; + public const string LuckStatus4 = "LuckStatus4"; + public const string LuckStatus5 = "LuckStatus5"; + public const string LuckStatus6 = "LuckStatus6"; + public const string RobinBuildingStatus = "RobinBuildingStatus"; + public const string RobinHouseUpgradeStatus = "RobinHouseUpgradeStatus"; + public const string NpcBirthday = "NpcBirthday"; + public const string CanFindSalmonberry = "CanFindSalmonberry"; + public const string CanFindBlackberry = "CanFindBlackberry"; + public const string CanFindHazelnut = "CanFindHazelnut"; + } } diff --git a/UIInfoSuite2/Infrastructure/Reflection.cs b/UIInfoSuite2/Infrastructure/Reflection.cs index bc4d35c0..fe3d9f7b 100644 --- a/UIInfoSuite2/Infrastructure/Reflection.cs +++ b/UIInfoSuite2/Infrastructure/Reflection.cs @@ -2,173 +2,218 @@ using System.Collections.Generic; using System.Reflection; -/// Reflector#GetPropertyGetter provides cached readonly access to properties through reflection. +/// Reflector#GetPropertyGetter provides cached readonly access to properties through reflection. /// Where TValue can be a supertype of the actual property type. /// Based on SMAPI's Reflector class. namespace UIInfoSuite2.Infrastructure.Reflection { - public interface IReflectedGetProperty - { - PropertyInfo PropertyInfo { get; } + public interface IReflectedGetProperty + { + PropertyInfo PropertyInfo { get; } - TValue GetValue(); - } + TValue GetValue(); + } - public class ReflectedGetProperty : IReflectedGetProperty - { - private readonly string DisplayName; - private readonly Func? GetMethod; + public class ReflectedGetProperty : IReflectedGetProperty + { + private readonly string DisplayName; + private readonly Func? GetMethod; - public PropertyInfo PropertyInfo { get; } - - public ReflectedGetProperty(Type parentType, object? obj, PropertyInfo property, bool isStatic) + public ReflectedGetProperty(Type parentType, object? obj, PropertyInfo property, bool isStatic) + { + // validate input + if (parentType == null) + { + throw new ArgumentNullException(nameof(parentType)); + } + + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + // validate static + if (isStatic && obj != null) + { + throw new ArgumentException("A static property cannot have an object instance."); + } + + if (!isStatic && obj == null) + { + throw new ArgumentException("A non-static property must have an object instance."); + } + + + DisplayName = $"{parentType.FullName}::{property.Name}"; + PropertyInfo = property; + + if (PropertyInfo.GetMethod != null) + { + try { - // validate input - if (parentType == null) - throw new ArgumentNullException(nameof(parentType)); - if (property == null) - throw new ArgumentNullException(nameof(property)); + GetMethod = (Func)Delegate.CreateDelegate(typeof(Func), obj, PropertyInfo.GetMethod); + } + catch (ArgumentException) + { + if (PropertyInfo.PropertyType.IsEnum) + { + Type enumType = PropertyInfo.PropertyType; + GetMethod = delegate + { + var enumDelegate = Delegate.CreateDelegate( + typeof(Func<>).MakeGenericType(Enum.GetUnderlyingType(enumType)), + obj, + PropertyInfo.GetMethod + ); + return (TValue)Enum.ToObject(enumType, enumDelegate.DynamicInvoke()!); + }; + } + else + { + throw; + } + } + } + } - // validate static - if (isStatic && obj != null) - throw new ArgumentException("A static property cannot have an object instance."); - if (!isStatic && obj == null) - throw new ArgumentException("A non-static property must have an object instance."); + public PropertyInfo PropertyInfo { get; } + public TValue GetValue() + { + if (GetMethod == null) + { + throw new InvalidOperationException($"The {DisplayName} property has no get method."); + } + + try + { + return GetMethod(); + } + catch (InvalidCastException) + { + throw new InvalidCastException( + $"Can't convert the {DisplayName} property from {PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}." + ); + } + catch (Exception ex) + { + throw new Exception($"Couldn't get the value of the {DisplayName} property", ex); + } + } + } - this.DisplayName = $"{parentType.FullName}::{property.Name}"; - this.PropertyInfo = property; + public class Reflector + { + private readonly IntervalMemoryCache Cache = new(); - if (this.PropertyInfo.GetMethod != null) - { - try - { - this.GetMethod = (Func) Delegate.CreateDelegate(typeof(Func), obj, this.PropertyInfo.GetMethod); - } - catch (ArgumentException) - { - if (this.PropertyInfo.PropertyType.IsEnum) - { - Type enumType = this.PropertyInfo.PropertyType; - this.GetMethod = delegate () { - var enumDelegate = Delegate.CreateDelegate(typeof(Func<>).MakeGenericType(Enum.GetUnderlyingType(enumType)), obj, this.PropertyInfo.GetMethod); - return (TValue) Enum.ToObject(enumType, enumDelegate.DynamicInvoke()!); - }; - } - else - { - throw; - } - } - } - } + public void NewCacheInterval() + { + Cache.StartNewInterval(); + } - public TValue GetValue() - { - if (this.GetMethod == null) - throw new InvalidOperationException($"The {this.DisplayName} property has no get method."); - - try - { - return this.GetMethod(); - } - catch (InvalidCastException) - { - throw new InvalidCastException($"Can't convert the {this.DisplayName} property from {this.PropertyInfo.PropertyType.FullName} to {typeof(TValue).FullName}."); - } - catch (Exception ex) - { - throw new Exception($"Couldn't get the value of the {this.DisplayName} property", ex); - } - } + public IReflectedGetProperty GetPropertyGetter(object obj, string name, bool required = true) + { + // validate + if (obj == null) + { + throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object."); + } + + // get property from hierarchy + IReflectedGetProperty? property = GetGetPropertyFromHierarchy( + obj.GetType(), + obj, + name, + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public + ); + if (required && property == null) + { + throw new InvalidOperationException( + $"The {obj.GetType().FullName} object doesn't have a '{name}' instance property." + ); + } + + return property!; } - public class Reflector + private IReflectedGetProperty? GetGetPropertyFromHierarchy( + Type type, + object? obj, + string name, + BindingFlags bindingFlags + ) { - public class IntervalMemoryCache - where TKey : notnull + bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); + PropertyInfo? property = GetCached( + 'p', + type, + name, + isStatic, + () => { - private Dictionary HotCache = new(); - private Dictionary StaleCache = new(); - - public TValue GetOrSet(TKey cacheKey, Func get) + for (Type? curType = type; curType != null; curType = curType.BaseType) + { + PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags); + if (propertyInfo != null) { - // from hot cache - if (this.HotCache.TryGetValue(cacheKey, out TValue? value)) - return value; - - // from stale cache - if (this.StaleCache.TryGetValue(cacheKey, out value)) - { - this.HotCache[cacheKey] = value; - return value; - } - - // new value - value = get(); - this.HotCache[cacheKey] = value; - return value; + type = curType; + return propertyInfo; } + } - public void StartNewInterval() - { - this.StaleCache.Clear(); - if (this.HotCache.Count is not 0) - (this.StaleCache, this.HotCache) = (this.HotCache, this.StaleCache); // swap hot cache to stale - } + return null; } + ); - private readonly IntervalMemoryCache Cache = new(); + return property != null ? new ReflectedGetProperty(type, obj, property, isStatic) : null; + } - public void NewCacheInterval() - { - this.Cache.StartNewInterval(); - } + private TMemberInfo? GetCached( + char memberType, + Type type, + string memberName, + bool isStatic, + Func fetch + ) where TMemberInfo : MemberInfo + { + var key = $"{memberType}{(isStatic ? 's' : 'i')}{type.FullName}:{memberName}"; + return (TMemberInfo?)Cache.GetOrSet(key, fetch); + } + + public class IntervalMemoryCache where TKey : notnull + { + private Dictionary HotCache = new(); + private Dictionary StaleCache = new(); - public IReflectedGetProperty GetPropertyGetter(object obj, string name, bool required = true) + public TValue GetOrSet(TKey cacheKey, Func get) + { + // from hot cache + if (HotCache.TryGetValue(cacheKey, out TValue? value)) { - // validate - if (obj == null) - throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object."); - - // get property from hierarchy - IReflectedGetProperty? property = this.GetGetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - if (required && property == null) - throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property."); - return property!; + return value; } - private IReflectedGetProperty? GetGetPropertyFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags) + // from stale cache + if (StaleCache.TryGetValue(cacheKey, out value)) { - bool isStatic = bindingFlags.HasFlag(BindingFlags.Static); - PropertyInfo? property = this.GetCached( - 'p', type, name, isStatic, - fetch: () => - { - for (Type? curType = type; curType != null; curType = curType.BaseType) - { - PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags); - if (propertyInfo != null) - { - type = curType; - return propertyInfo; - } - } - - return null; - } - ); - - return property != null - ? new ReflectedGetProperty(type, obj, property, isStatic) - : null; + HotCache[cacheKey] = value; + return value; } - private TMemberInfo? GetCached(char memberType, Type type, string memberName, bool isStatic, Func fetch) - where TMemberInfo : MemberInfo + // new value + value = get(); + HotCache[cacheKey] = value; + return value; + } + + public void StartNewInterval() + { + StaleCache.Clear(); + if (HotCache.Count is not 0) { - string key = $"{memberType}{(isStatic ? 's' : 'i')}{type.FullName}:{memberName}"; - return (TMemberInfo?) this.Cache.GetOrSet(key, fetch); + (StaleCache, HotCache) = (HotCache, StaleCache); // swap hot cache to stale } + } } + } } diff --git a/UIInfoSuite2/Infrastructure/Tools.cs b/UIInfoSuite2/Infrastructure/Tools.cs index f3357cdc..d3b95614 100644 --- a/UIInfoSuite2/Infrastructure/Tools.cs +++ b/UIInfoSuite2/Infrastructure/Tools.cs @@ -1,200 +1,189 @@ using System; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; +using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewValley; +using StardewValley.GameData.Crops; +using StardewValley.GameData.FruitTrees; using StardewValley.Menus; +using StardewValley.TerrainFeatures; +using UIInfoSuite2.Compatibility; using SObject = StardewValley.Object; namespace UIInfoSuite2.Infrastructure { - public static class Tools + public static class Tools + { + public static int GetWidthInPlayArea() { - public static void CreateSafeDelayedDialogue(string dialogue, int timer) - { - Task.Factory.StartNew(() => - { - Thread.Sleep(timer); - - do - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - } - while (Game1.activeClickableMenu is GameMenu); - Game1.setDialogue(dialogue, true); - }); - } + if (Game1.isOutdoorMapSmallerThanViewport()) + { + int right = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right; + int totalWidth = Game1.currentLocation.map.Layers[0].LayerWidth * Game1.tileSize; + int someOtherWidth = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right - totalWidth; - public static int GetWidthInPlayArea() - { - if (Game1.isOutdoorMapSmallerThanViewport()) - { - int right = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right; - int totalWidth = Game1.currentLocation.map.Layers[0].LayerWidth * Game1.tileSize; - int someOtherWidth = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right - totalWidth; + return right - someOtherWidth / 2; + } - return right - someOtherWidth / 2; - } - else - { - return Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right; - } - } + return Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right; + } + + public static int GetSellToStorePrice(Item item) + { + if (item is SObject obj) + { + return obj.sellToStorePrice(); + } + + return item.salePrice() / 2; + } - public static int GetSellToStorePrice(Item item) + public static SObject? GetHarvest(Item item) + { + if (item is SObject { Category: SObject.SeedsCategory } seedsObject && seedsObject.ItemId != Crop.mixedSeedsId) + { + if (seedsObject.IsFruitTreeSapling() && FruitTree.TryGetData(item.ItemId, out FruitTreeData? fruitTreeData)) { - if (item is SObject obj) - { - return obj.sellToStorePrice(); - } - else - { - return item.salePrice() / 2; - } + // TODO support multiple items returned + return ItemRegistry.Create(fruitTreeData.Fruit[0].ItemId); } - public static SObject? GetHarvest(Item item) + if (ModEntry.DGA.IsCustomObject(item, out DynamicGameAssetsHelper? dgaHelper)) { - if (item is SObject seedsObject - && seedsObject.Category == StardewValley.Object.SeedsCategory - && seedsObject.ParentSheetIndex != Crop.mixedSeedIndex) + try + { + return dgaHelper.GetSeedsHarvest(item); + } + catch (Exception e) + { + string? itemId = null; + try { - if (seedsObject.isSapling()) - { - var tree = new StardewValley.TerrainFeatures.FruitTree(seedsObject.ParentSheetIndex); - return new SObject(tree.indexOfFruit.Value, 1); - } - else if (ModEntry.DGA.IsCustomObject(item, out var dgaHelper)) - { - try - { - return dgaHelper.GetSeedsHarvest(item); - } - catch (Exception e) - { - string? itemId = null; - try - { - itemId = dgaHelper.GetFullId(item); - } - catch (Exception catchException) - { - ModEntry.MonitorObject.Log(catchException.ToString(), LogLevel.Trace); - } - ModEntry.MonitorObject.LogOnce($"An error occured while fetching the harvest for {itemId ?? "unknownItem"}", LogLevel.Error); - ModEntry.MonitorObject.Log(e.ToString(), LogLevel.Debug); - return null; - } - } - else - { - var crop = new Crop(seedsObject.ParentSheetIndex, 0, 0); - return new SObject(crop.indexOfHarvest.Value, 1); - } - } else { - return null; + itemId = dgaHelper.GetFullId(item); + } + catch (Exception catchException) + { + ModEntry.MonitorObject.Log(catchException.ToString()); } - } - public static int GetHarvestPrice(Item item) - { - return GetHarvest(item)?.sellToStorePrice() ?? 0; + ModEntry.MonitorObject.LogOnce( + $"An error occured while fetching the harvest for {itemId ?? "unknownItem"}", + LogLevel.Error + ); + ModEntry.MonitorObject.Log(e.ToString(), LogLevel.Debug); + return null; + } } - public static void DrawMouseCursor() + if (Crop.TryGetData(item.ItemId, out CropData cropData) && cropData.HarvestItemId is not null) { - if (!Game1.options.hardwareCursor) - { - int mouseCursorToRender = Game1.options.gamepadControls ? Game1.mouseCursor + 44 : Game1.mouseCursor; - var what = Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, mouseCursorToRender, 16, 16); - - Game1.spriteBatch.Draw( - Game1.mouseCursors, - new Vector2(Game1.getMouseX(), Game1.getMouseY()), - what, - Color.White, - 0.0f, - Vector2.Zero, - Game1.pixelZoom + (Game1.dialogueButtonScale / 150.0f), - SpriteEffects.None, - 1f); - } + return ItemRegistry.Create(cropData.HarvestItemId); } + } - public static Item? GetHoveredItem() - { - Item? hoverItem = null; + return null; + } - if (Game1.activeClickableMenu == null && Game1.onScreenMenus != null) - { - foreach (var menu in Game1.onScreenMenus) - { - if (menu is Toolbar toolbar) - { - FieldInfo hoverItemField = typeof(Toolbar).GetField("hoverItem", BindingFlags.Instance | BindingFlags.NonPublic); - hoverItem = hoverItemField.GetValue(toolbar) as Item; - } - } - } + public static int GetHarvestPrice(Item item) + { + return GetHarvest(item)?.sellToStorePrice() ?? 0; + } - if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.GetCurrentPage() is InventoryPage inventory) - { - FieldInfo hoveredItemField = typeof(InventoryPage).GetField("hoveredItem", BindingFlags.Instance | BindingFlags.NonPublic); - hoverItem = hoveredItemField.GetValue(inventory) as Item; - } + public static void DrawMouseCursor() + { + if (!Game1.options.hardwareCursor) + { + int mouseCursorToRender = Game1.options.gamepadControls ? Game1.mouseCursor + 44 : Game1.mouseCursor; + Rectangle what = Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, mouseCursorToRender, 16, 16); + + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(Game1.getMouseX(), Game1.getMouseY()), + what, + Color.White, + 0.0f, + Vector2.Zero, + Game1.pixelZoom + Game1.dialogueButtonScale / 150.0f, + SpriteEffects.None, + 1f + ); + } + } - if (Game1.activeClickableMenu is ItemGrabMenu itemMenu) - { - hoverItem = itemMenu.hoveredItem; - } + public static Item? GetHoveredItem() + { + Item? hoverItem = null; + + if (Game1.activeClickableMenu == null && Game1.onScreenMenus != null) + { + hoverItem = Game1.onScreenMenus.OfType() + .Select(tb => tb.hoverItem) + .FirstOrDefault(hi => hi is not null); + } + + if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.GetCurrentPage() is InventoryPage inventory) + { + hoverItem = inventory.hoveredItem; + } + + if (Game1.activeClickableMenu is ItemGrabMenu itemMenu) + { + hoverItem = itemMenu.hoveredItem; + } + + return hoverItem; + } - return hoverItem; + public static void GetSubTexture(Color[] output, Color[] originalColors, Rectangle sourceBounds, Rectangle clipArea) + { + if (output.Length < clipArea.Width * clipArea.Height) + { + return; + } + + var dest = 0; + for (var yOffset = 0; yOffset < clipArea.Height; yOffset++) + { + for (var xOffset = 0; xOffset < clipArea.Width; xOffset++) + { + int idx = clipArea.X + xOffset + sourceBounds.Width * (yOffset + clipArea.Y); + output[dest++] = originalColors[idx]; } + } + } - public static void GetSubTexture(Color[] output, Color[] originalColors, Rectangle sourceBounds, Rectangle clipArea) + public static void SetSubTexture( + Color[] sourceColors, + Color[] destColors, + int destWidth, + Rectangle destBounds, + bool overlay = false + ) + { + if (sourceColors.Length > destColors.Length || destBounds.Width * destBounds.Height > destColors.Length) + { + return; + } + + var emptyColor = new Color(0, 0, 0, 0); + var srcIdx = 0; + for (var yOffset = 0; yOffset < destBounds.Height; yOffset++) + { + for (var xOffset = 0; xOffset < destBounds.Width; xOffset++) { - if (output.Length < clipArea.Width * clipArea.Height) - { - return; - } + int idx = destBounds.X + xOffset + destWidth * (yOffset + destBounds.Y); + Color sourcePixel = sourceColors[srcIdx++]; - var dest = 0; - for (var yOffset = 0; yOffset < clipArea.Height; yOffset++) - { - for (var xOffset = 0; xOffset < clipArea.Width; xOffset++) - { - var idx = (clipArea.X + xOffset) + (sourceBounds.Width * (yOffset + clipArea.Y)); - output[dest++] = originalColors[idx]; - } - } + // If using overlay mode, don't copy transparent pixels + if (overlay && emptyColor.Equals(sourcePixel)) + { + continue; + } + destColors[idx] = sourcePixel; } - - public static void SetSubTexture(Color[] sourceColors, Color[] destColors, int destWidth, Rectangle destBounds, bool overlay = false) - { - if(sourceColors.Length > destColors.Length || (destBounds.Width * destBounds.Height) > destColors.Length) { - return; - } - var emptyColor = new Color(0, 0, 0, 0); - var srcIdx = 0; - for (var yOffset = 0; yOffset < destBounds.Height; yOffset++) - { - for (var xOffset = 0; xOffset < destBounds.Width; xOffset++) - { - var idx = (destBounds.X + xOffset) + (destWidth * (yOffset + destBounds.Y)); - Color sourcePixel = sourceColors[srcIdx++]; - - // If using overlay mode, don't copy transparent pixels - if (overlay && emptyColor.Equals(sourcePixel)) - { - continue; - } - destColors[idx] = sourcePixel; - } - } - } + } } + } } diff --git a/UIInfoSuite2/ModEntry.cs b/UIInfoSuite2/ModEntry.cs index b4fe40be..cdb48c36 100644 --- a/UIInfoSuite2/ModEntry.cs +++ b/UIInfoSuite2/ModEntry.cs @@ -1,8 +1,8 @@ -using StardewModdingAPI; +using System; +using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; using UIInfoSuite2.AdditionalFeatures; using UIInfoSuite2.Compatibility; using UIInfoSuite2.Infrastructure; @@ -10,156 +10,166 @@ namespace UIInfoSuite2 { - public class ModEntry : Mod + public class ModEntry : Mod + { + #region Entry + public override void Entry(IModHelper helper) { + MonitorObject = Monitor; + DGA = new DynamicGameAssetsEntry(Helper, Monitor); - #region Properties - public static IMonitor MonitorObject { get; private set; } - public static DynamicGameAssetsEntry DGA { get; private set; } + _skipIntro = new SkipIntro(helper.Events); + _modConfig = Helper.ReadConfig(); - private static SkipIntro _skipIntro; // Needed so GC won't throw away object with subscriptions - private static ModConfig _modConfig; + helper.Events.GameLoop.ReturnedToTitle += OnReturnedToTitle; + helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; + helper.Events.GameLoop.Saved += OnSaved; + helper.Events.GameLoop.GameLaunched += OnGameLaunched; + helper.Events.Display.Rendering += IconHandler.Handler.Reset; + } + #endregion - private ModOptions _modOptions; - private ModOptionsPageHandler _modOptionsPageHandler; + #region Generic mod config menu + private void OnGameLaunched(object sender, GameLaunchedEventArgs e) + { + // get Generic Mod Config Menu's API (if it's installed) + ISemanticVersion? modVersion = Helper.ModRegistry.Get("spacechase0.GenericModConfigMenu")?.Manifest?.Version; + var minModVersion = "1.6.0"; + if (modVersion?.IsOlderThan(minModVersion) == true) + { + Monitor.Log( + $"Detected Generic Mod Config Menu {modVersion} but expected {minModVersion} or newer. Disabling integration with that mod.", + LogLevel.Warn + ); + return; + } + + var configMenu = Helper.ModRegistry.GetApi("spacechase0.GenericModConfigMenu"); + if (configMenu is null) + { + return; + } + + // register mod + configMenu.Register(ModManifest, () => _modConfig = new ModConfig(), () => Helper.WriteConfig(_modConfig)); + + // add some config options + configMenu.AddBoolOption( + ModManifest, + name: () => "Show options in in-game menu", + tooltip: () => "Enables an extra tab in the in-game menu where you can configure every options for this mod.", + getValue: () => _modConfig.ShowOptionsTabInMenu, + setValue: value => _modConfig.ShowOptionsTabInMenu = value + ); + configMenu.AddTextOption( + ModManifest, + name: () => "Apply default settings from this save", + tooltip: () => "New characters will inherit the settings for the mod from this save file.", + getValue: () => _modConfig.ApplyDefaultSettingsFromThisSave, + setValue: value => _modConfig.ApplyDefaultSettingsFromThisSave = value + ); + configMenu.AddKeybindList( + ModManifest, + name: () => "Open calendar keybind", + tooltip: () => "Opens the calendar tab.", + getValue: () => _modConfig.OpenCalendarKeybind, + setValue: value => _modConfig.OpenCalendarKeybind = value + ); + configMenu.AddKeybindList( + ModManifest, + name: () => "Open quest board keybind", + tooltip: () => "Opens the quest board.", + getValue: () => _modConfig.OpenQuestBoardKeybind, + setValue: value => _modConfig.OpenQuestBoardKeybind = value + ); + } + #endregion - private static EventHandler _calendarAndQuestKeyBindingsHandler; - #endregion + #region Properties + public static IMonitor MonitorObject { get; private set; } + public static DynamicGameAssetsEntry DGA { get; private set; } + private static SkipIntro _skipIntro; // Needed so GC won't throw away object with subscriptions + private static ModConfig _modConfig; - #region Entry - public override void Entry(IModHelper helper) - { - MonitorObject = Monitor; - DGA = new DynamicGameAssetsEntry(Helper, Monitor); + private ModOptions _modOptions; + private ModOptionsPageHandler _modOptionsPageHandler; - _skipIntro = new SkipIntro(helper.Events); - _modConfig = Helper.ReadConfig(); - - helper.Events.GameLoop.ReturnedToTitle += OnReturnedToTitle; - helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; - helper.Events.GameLoop.Saved += OnSaved; - helper.Events.GameLoop.GameLaunched += OnGameLaunched; - helper.Events.Display.Rendering += IconHandler.Handler.Reset; - } - #endregion + private static EventHandler _calendarAndQuestKeyBindingsHandler; + #endregion - #region Event subscriptions - private void OnReturnedToTitle(object sender, ReturnedToTitleEventArgs e) - { - // Unload if the main player quits. - if (Context.ScreenId != 0) return; - - _modOptionsPageHandler?.Dispose(); - _modOptionsPageHandler = null; - } + #region Event subscriptions + private void OnReturnedToTitle(object sender, ReturnedToTitleEventArgs e) + { + // Unload if the main player quits. + if (Context.ScreenId != 0) + { + return; + } + + _modOptionsPageHandler?.Dispose(); + _modOptionsPageHandler = null; + } - private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) - { - // Only load once for split screen. - if (Context.ScreenId != 0) return; + private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) + { + // Only load once for split screen. + if (Context.ScreenId != 0) + { + return; + } - _modOptions = Helper.Data.ReadJsonFile($"data/{Constants.SaveFolderName}.json") - ?? Helper.Data.ReadJsonFile($"data/{_modConfig.ApplyDefaultSettingsFromThisSave}.json") - ?? new ModOptions(); + _modOptions = Helper.Data.ReadJsonFile($"data/{Constants.SaveFolderName}.json") ?? + Helper.Data.ReadJsonFile($"data/{_modConfig.ApplyDefaultSettingsFromThisSave}.json") ?? + new ModOptions(); - _modOptionsPageHandler = new ModOptionsPageHandler(Helper, _modOptions, _modConfig.ShowOptionsTabInMenu); - } + _modOptionsPageHandler = new ModOptionsPageHandler(Helper, _modOptions, _modConfig.ShowOptionsTabInMenu); + } - private void OnSaved(object sender, EventArgs e) - { - // Only save for the main player. - if (Context.ScreenId != 0) return; + private void OnSaved(object sender, EventArgs e) + { + // Only save for the main player. + if (Context.ScreenId != 0) + { + return; + } - Helper.Data.WriteJsonFile($"data/{Constants.SaveFolderName}.json", _modOptions); - } + Helper.Data.WriteJsonFile($"data/{Constants.SaveFolderName}.json", _modOptions); + } - public static void RegisterCalendarAndQuestKeyBindings(IModHelper helper, bool subscribe) - { - if (_calendarAndQuestKeyBindingsHandler == null) - _calendarAndQuestKeyBindingsHandler = (object sender, ButtonsChangedEventArgs e) => HandleCalendarAndQuestKeyBindings(helper); + public static void RegisterCalendarAndQuestKeyBindings(IModHelper helper, bool subscribe) + { + if (_calendarAndQuestKeyBindingsHandler == null) + { + _calendarAndQuestKeyBindingsHandler = (sender, e) => HandleCalendarAndQuestKeyBindings(helper); + } - helper.Events.Input.ButtonsChanged -= _calendarAndQuestKeyBindingsHandler; + helper.Events.Input.ButtonsChanged -= _calendarAndQuestKeyBindingsHandler; - if (subscribe) - { - helper.Events.Input.ButtonsChanged += _calendarAndQuestKeyBindingsHandler; - } - } + if (subscribe) + { + helper.Events.Input.ButtonsChanged += _calendarAndQuestKeyBindingsHandler; + } + } - private static void HandleCalendarAndQuestKeyBindings(IModHelper helper) + private static void HandleCalendarAndQuestKeyBindings(IModHelper helper) + { + if (_modConfig != null) + { + if (Context.IsPlayerFree && _modConfig.OpenCalendarKeybind.JustPressed()) { - if (_modConfig != null) - { - if (Context.IsPlayerFree && _modConfig.OpenCalendarKeybind.JustPressed()) - { - helper.Input.SuppressActiveKeybinds(_modConfig.OpenCalendarKeybind); - Game1.activeClickableMenu = new Billboard(false); - } - else if (Context.IsPlayerFree && _modConfig.OpenQuestBoardKeybind.JustPressed()) - { - helper.Input.SuppressActiveKeybinds(_modConfig.OpenQuestBoardKeybind); - Game1.RefreshQuestOfTheDay(); - Game1.activeClickableMenu = new Billboard(true); - } - } + helper.Input.SuppressActiveKeybinds(_modConfig.OpenCalendarKeybind); + Game1.activeClickableMenu = new Billboard(); } - #endregion - - #region Generic mod config menu - private void OnGameLaunched(object sender, GameLaunchedEventArgs e) + else if (Context.IsPlayerFree && _modConfig.OpenQuestBoardKeybind.JustPressed()) { - // get Generic Mod Config Menu's API (if it's installed) - var modVersion = Helper.ModRegistry.Get("spacechase0.GenericModConfigMenu")?.Manifest?.Version; - var minModVersion = "1.6.0"; - if (modVersion?.IsOlderThan(minModVersion) == true) - { - Monitor.Log($"Detected Generic Mod Config Menu {modVersion} but expected {minModVersion} or newer. Disabling integration with that mod.", LogLevel.Warn); - return; - } - - var configMenu = Helper.ModRegistry.GetApi("spacechase0.GenericModConfigMenu"); - if (configMenu is null) - return; - - // register mod - configMenu.Register( - mod: ModManifest, - reset: () => _modConfig = new ModConfig(), - save: () => Helper.WriteConfig(_modConfig) - ); - - // add some config options - configMenu.AddBoolOption( - mod: ModManifest, - name: () => "Show options in in-game menu", - tooltip: () => "Enables an extra tab in the in-game menu where you can configure every options for this mod.", - getValue: () => _modConfig.ShowOptionsTabInMenu, - setValue: value => _modConfig.ShowOptionsTabInMenu = value - ); - configMenu.AddTextOption( - mod: ModManifest, - name: () => "Apply default settings from this save", - tooltip: () => "New characters will inherit the settings for the mod from this save file.", - getValue: () => _modConfig.ApplyDefaultSettingsFromThisSave, - setValue: value => _modConfig.ApplyDefaultSettingsFromThisSave = value - ); - configMenu.AddKeybindList( - mod: ModManifest, - name: () => "Open calendar keybind", - tooltip: () => "Opens the calendar tab.", - getValue: () => _modConfig.OpenCalendarKeybind, - setValue: value => _modConfig.OpenCalendarKeybind = value - ); - configMenu.AddKeybindList( - mod: ModManifest, - name: () => "Open quest board keybind", - tooltip: () => "Opens the quest board.", - getValue: () => _modConfig.OpenQuestBoardKeybind, - setValue: value => _modConfig.OpenQuestBoardKeybind = value - ); + helper.Input.SuppressActiveKeybinds(_modConfig.OpenQuestBoardKeybind); + Game1.RefreshQuestOfTheDay(); + Game1.activeClickableMenu = new Billboard(true); } - #endregion + } } + #endregion + } } diff --git a/UIInfoSuite2/Options/ModConfig.cs b/UIInfoSuite2/Options/ModConfig.cs index 4a3c9a28..dd8d1290 100644 --- a/UIInfoSuite2/Options/ModConfig.cs +++ b/UIInfoSuite2/Options/ModConfig.cs @@ -3,11 +3,11 @@ namespace UIInfoSuite2.Options { - internal class ModConfig - { - public bool ShowOptionsTabInMenu { get; set; } = true; - public string ApplyDefaultSettingsFromThisSave { get; set; } = "JohnDoe_123456789"; - public KeybindList OpenCalendarKeybind { get; set; } = KeybindList.ForSingle(SButton.B); - public KeybindList OpenQuestBoardKeybind { get; set; } = KeybindList.ForSingle(SButton.H); - } + internal class ModConfig + { + public bool ShowOptionsTabInMenu { get; set; } = true; + public string ApplyDefaultSettingsFromThisSave { get; set; } = "JohnDoe_123456789"; + public KeybindList OpenCalendarKeybind { get; set; } = KeybindList.ForSingle(SButton.B); + public KeybindList OpenQuestBoardKeybind { get; set; } = KeybindList.ForSingle(SButton.H); + } } diff --git a/UIInfoSuite2/Options/ModOptions.cs b/UIInfoSuite2/Options/ModOptions.cs index a4c3c8bc..c84cd7c6 100644 --- a/UIInfoSuite2/Options/ModOptions.cs +++ b/UIInfoSuite2/Options/ModOptions.cs @@ -2,35 +2,35 @@ namespace UIInfoSuite2.Options { - internal record ModOptions - { - public bool AllowExperienceBarToFadeOut { get; set; } = true; - public bool ShowExperienceBar { get; set; } = true; - public bool ShowExperienceGain { get; set; } = true; - public bool ShowLevelUpAnimation { get; set; } = true; - public bool ShowHeartFills { get; set; } = true; - public bool ShowExtraItemInformation { get; set; } = true; - public bool ShowLocationOfTownsPeople { get; set; } = true; - public bool ShowLuckIcon { get; set; } = true; - public bool ShowTravelingMerchant { get; set; } = true; - public bool ShowRainyDay { get; set; } = true; - public bool ShowCropAndBarrelTooltip { get; set; } = true; - public bool ShowBirthdayIcon { get; set; } = true; - public bool ShowAnimalsNeedPets { get; set; } = true; - public bool HideAnimalPetOnMaxFriendship { get; set; } = true; - public bool ShowItemEffectRanges { get; set; } = true; - public bool ShowItemsRequiredForBundles { get; set; } = true; - public bool ShowHarvestPricesInShop { get; set; } = true; - public bool DisplayCalendarAndBillboard { get; set; } = true; - public bool ShowWhenNewRecipesAreAvailable { get; set; } = true; - public bool ShowToolUpgradeStatus { get; set; } = true; - public bool HideMerchantWhenVisited { get; set; } = false; - public bool ShowExactValue { get; set; } = false; - public bool ShowRobinBuildingStatusIcon { get; set; } = true; - public bool ShowSeasonalBerry { get; set; } = true; - public bool ShowSeasonalBerryHazelnut { get; set; } = false; - public bool ShowTodaysGifts { get; set; } = true; - public bool HideBirthdayIfFullFriendShip { get; set; } = true; - public Dictionary ShowLocationOfFriends { get; set; } = new(); - } + internal record ModOptions + { + public bool AllowExperienceBarToFadeOut { get; set; } = true; + public bool ShowExperienceBar { get; set; } = true; + public bool ShowExperienceGain { get; set; } = true; + public bool ShowLevelUpAnimation { get; set; } = true; + public bool ShowHeartFills { get; set; } = true; + public bool ShowExtraItemInformation { get; set; } = true; + public bool ShowLocationOfTownsPeople { get; set; } = true; + public bool ShowLuckIcon { get; set; } = true; + public bool ShowTravelingMerchant { get; set; } = true; + public bool ShowRainyDay { get; set; } = true; + public bool ShowCropAndBarrelTooltip { get; set; } = true; + public bool ShowBirthdayIcon { get; set; } = true; + public bool ShowAnimalsNeedPets { get; set; } = true; + public bool HideAnimalPetOnMaxFriendship { get; set; } = true; + public bool ShowItemEffectRanges { get; set; } = true; + public bool ShowItemsRequiredForBundles { get; set; } = true; + public bool ShowHarvestPricesInShop { get; set; } = true; + public bool DisplayCalendarAndBillboard { get; set; } = true; + public bool ShowWhenNewRecipesAreAvailable { get; set; } = true; + public bool ShowToolUpgradeStatus { get; set; } = true; + public bool HideMerchantWhenVisited { get; set; } = false; + public bool ShowExactValue { get; set; } = false; + public bool ShowRobinBuildingStatusIcon { get; set; } = true; + public bool ShowSeasonalBerry { get; set; } = true; + public bool ShowSeasonalBerryHazelnut { get; set; } = false; + public bool ShowTodaysGifts { get; set; } = true; + public bool HideBirthdayIfFullFriendShip { get; set; } = true; + public Dictionary ShowLocationOfFriends { get; set; } = new(); + } } diff --git a/UIInfoSuite2/Options/ModOptionsCheckbox.cs b/UIInfoSuite2/Options/ModOptionsCheckbox.cs index 780ad84d..798ca989 100644 --- a/UIInfoSuite2/Options/ModOptionsCheckbox.cs +++ b/UIInfoSuite2/Options/ModOptionsCheckbox.cs @@ -1,56 +1,67 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; using StardewValley.Menus; -using System; namespace UIInfoSuite2.Options { - internal class ModOptionsCheckbox : ModOptionsElement + internal class ModOptionsCheckbox : ModOptionsElement + { + private readonly Action _setOption; + private readonly Action _toggleOptionsDelegate; + private bool _isChecked; + + public ModOptionsCheckbox( + string label, + int whichOption, + Action toggleOptionDelegate, + Func getOption, + Action setOption, + ModOptionsCheckbox parent = null + ) : base(label, whichOption, parent) { - private readonly Action _toggleOptionsDelegate; - private bool _isChecked; - private readonly Action _setOption; - private bool _canClick => !(_parent is ModOptionsCheckbox) || (_parent as ModOptionsCheckbox)._isChecked; + _toggleOptionsDelegate = toggleOptionDelegate; + _setOption = setOption; - public ModOptionsCheckbox( - string label, - int whichOption, - Action toggleOptionDelegate, - Func getOption, - Action setOption, - ModOptionsCheckbox parent = null) - : base(label, whichOption, parent) - { - _toggleOptionsDelegate = toggleOptionDelegate; - _setOption = setOption; + _isChecked = getOption(); + _toggleOptionsDelegate(_isChecked); + } - _isChecked = getOption(); - _toggleOptionsDelegate(_isChecked); - } + private bool _canClick => !(_parent is ModOptionsCheckbox) || (_parent as ModOptionsCheckbox)._isChecked; - public override void ReceiveLeftClick(int x, int y) - { - if (_canClick) - { - Game1.playSound("drumkit6"); - base.ReceiveLeftClick(x, y); - _isChecked = !_isChecked; - _setOption(_isChecked); - _toggleOptionsDelegate(_isChecked); - } - } + public override void ReceiveLeftClick(int x, int y) + { + if (_canClick) + { + Game1.playSound("drumkit6"); + base.ReceiveLeftClick(x, y); + _isChecked = !_isChecked; + _setOption(_isChecked); + _toggleOptionsDelegate(_isChecked); + } + } - public override void Draw(SpriteBatch batch, int slotX, int slotY) - { - batch.Draw(Game1.mouseCursors, new Vector2(slotX + Bounds.X, slotY + Bounds.Y), new Rectangle?(_isChecked ? OptionsCheckbox.sourceRectChecked : OptionsCheckbox.sourceRectUnchecked), Color.White * (_canClick ? 1f : 0.33f), 0.0f, Vector2.Zero, Game1.pixelZoom, SpriteEffects.None, 0.4f); - base.Draw(batch, slotX, slotY); - } + public override void Draw(SpriteBatch batch, int slotX, int slotY) + { + batch.Draw( + Game1.mouseCursors, + new Vector2(slotX + Bounds.X, slotY + Bounds.Y), + _isChecked ? OptionsCheckbox.sourceRectChecked : OptionsCheckbox.sourceRectUnchecked, + Color.White * (_canClick ? 1f : 0.33f), + 0.0f, + Vector2.Zero, + Game1.pixelZoom, + SpriteEffects.None, + 0.4f + ); + base.Draw(batch, slotX, slotY); + } - public override Point? GetRelativeSnapPoint(Rectangle slotBounds) - { - // Based on the value calculated in OptionsPage.snapCursorToCurrentSnappedComponent - return new Point(Bounds.X + 16, Bounds.Y + 13); - } + public override Point? GetRelativeSnapPoint(Rectangle slotBounds) + { + // Based on the value calculated in OptionsPage.snapCursorToCurrentSnappedComponent + return new Point(Bounds.X + 16, Bounds.Y + 13); } + } } diff --git a/UIInfoSuite2/Options/ModOptionsElement.cs b/UIInfoSuite2/Options/ModOptionsElement.cs index c3f2446c..edf0f8a5 100644 --- a/UIInfoSuite2/Options/ModOptionsElement.cs +++ b/UIInfoSuite2/Options/ModOptionsElement.cs @@ -6,79 +6,81 @@ namespace UIInfoSuite2.Options { - public class ModOptionsElement - { - protected const int DefaultX = 8; - protected const int DefaultY = 4; - protected const int DefaultPixelSize = 9; - - private Rectangle _bounds; - private string _label; - private int _whichOption; - - protected readonly ModOptionsElement _parent; - - public Rectangle Bounds => _bounds; - - public ModOptionsElement(string label, int whichOption = -1, ModOptionsElement parent = null) - { - int x = DefaultX * Game1.pixelZoom; - int y = DefaultY * Game1.pixelZoom; - int width = DefaultPixelSize * Game1.pixelZoom; - int height = DefaultPixelSize * Game1.pixelZoom; + public class ModOptionsElement + { + protected const int DefaultX = 8; + protected const int DefaultY = 4; + protected const int DefaultPixelSize = 9; - if (parent != null) - x += DefaultX * 2 * Game1.pixelZoom; + private readonly Rectangle _bounds; + private readonly string _label; - _bounds = new Rectangle(x, y, width, height); - _label = label; - _whichOption = whichOption; + protected readonly ModOptionsElement _parent; + private readonly int _whichOption; - _parent = parent; - } + public ModOptionsElement(string label, int whichOption = -1, ModOptionsElement parent = null) + { + int x = DefaultX * Game1.pixelZoom; + int y = DefaultY * Game1.pixelZoom; + int width = DefaultPixelSize * Game1.pixelZoom; + int height = DefaultPixelSize * Game1.pixelZoom; - public virtual void ReceiveLeftClick(int x, int y) - { + if (parent != null) + { + x += DefaultX * 2 * Game1.pixelZoom; + } - } + _bounds = new Rectangle(x, y, width, height); + _label = label; + _whichOption = whichOption; - public virtual void LeftClickHeld(int x, int y) - { + _parent = parent; + } - } + public Rectangle Bounds => _bounds; - public virtual void LeftClickReleased(int x, int y) - { + public virtual void ReceiveLeftClick(int x, int y) { } - } + public virtual void LeftClickHeld(int x, int y) { } - public virtual void ReceiveKeyPress(Keys key) - { + public virtual void LeftClickReleased(int x, int y) { } - } + public virtual void ReceiveKeyPress(Keys key) { } - public virtual void Draw(SpriteBatch batch, int slotX, int slotY) - { - if (_whichOption < 0) - { - SpriteText.drawString(batch, _label, slotX + _bounds.X, slotY + _bounds.Y + Game1.pixelZoom * 3, 999, -1, 999, 1, 0.1f); - } - else - { - Utility.drawTextWithShadow(batch, - _label, - Game1.dialogueFont, - new Vector2(slotX + _bounds.X + _bounds.Width + Game1.pixelZoom * 2, slotY + _bounds.Y), - Game1.textColor, - 1f, - 0.1f); - } - } + public virtual void Draw(SpriteBatch batch, int slotX, int slotY) + { + if (_whichOption < 0) + { + SpriteText.drawString( + batch, + _label, + slotX + _bounds.X, + slotY + _bounds.Y + Game1.pixelZoom * 3, + 999, + -1, + 999, + 1, + 0.1f + ); + } + else + { + Utility.drawTextWithShadow( + batch, + _label, + Game1.dialogueFont, + new Vector2(slotX + _bounds.X + _bounds.Width + Game1.pixelZoom * 2, slotY + _bounds.Y), + Game1.textColor, + 1f, + 0.1f + ); + } + } - public virtual Point? GetRelativeSnapPoint(Rectangle slotBounds) - { - // Positioning taken from OptionsPage.snapCursorToCurrentSnappedComponent - return new Point(48, slotBounds.Height / 2 - 12); - } + public virtual Point? GetRelativeSnapPoint(Rectangle slotBounds) + { + // Positioning taken from OptionsPage.snapCursorToCurrentSnappedComponent + return new Point(48, slotBounds.Height / 2 - 12); } + } } diff --git a/UIInfoSuite2/Options/ModOptionsPage.cs b/UIInfoSuite2/Options/ModOptionsPage.cs index d6e321da..ab55d943 100644 --- a/UIInfoSuite2/Options/ModOptionsPage.cs +++ b/UIInfoSuite2/Options/ModOptionsPage.cs @@ -1,458 +1,494 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; -using System.Collections.Generic; namespace UIInfoSuite2.Options { - /// Our mod options made page to be added to - public class ModOptionsPage : IClickableMenu + /// Our mod options made page to be added to + public class ModOptionsPage : IClickableMenu + { + private const int visibleSlots = 7; + private const int Width = 800; + private readonly ClickableTextureComponent _downArrow; + private readonly List _options; + private readonly ClickableTextureComponent _scrollBar; + private readonly ClickableTextureComponent _upArrow; + private int _currentItemIndex; + private string _hoverText; + private bool _isScrolling; + + /// + /// The visible option slots + /// + /// Must be public so + /// can find it. + /// + /// + public List _optionSlots = new(); + + private int _optionsSlotHeld = -1; + private Rectangle _scrollBarRunner; + + public ModOptionsPage(List options, IModEvents events) : base( + Game1.activeClickableMenu.xPositionOnScreen, + Game1.activeClickableMenu.yPositionOnScreen + 10, + Width, + Game1.activeClickableMenu.height + ) { - private const int visibleSlots = 7; - private const int Width = 800; - - /// The visible option slots Must be public so - /// can find it. - public List _optionSlots = new(); - private List _options; - private string _hoverText; - private int _optionsSlotHeld = -1; - private int _currentItemIndex; - private bool _isScrolling; - private ClickableTextureComponent _upArrow; - private ClickableTextureComponent _downArrow; - private ClickableTextureComponent _scrollBar; - private Rectangle _scrollBarRunner; - - public ModOptionsPage(List options, IModEvents events) - : base(Game1.activeClickableMenu.xPositionOnScreen, Game1.activeClickableMenu.yPositionOnScreen + 10, Width, Game1.activeClickableMenu.height) + _options = options; + _upArrow = new ClickableTextureComponent( + new Rectangle( + xPositionOnScreen + width + Game1.tileSize / 4, + yPositionOnScreen + Game1.tileSize, + 11 * Game1.pixelZoom, + 12 * Game1.pixelZoom + ), + Game1.mouseCursors, + new Rectangle(421, 459, 11, 12), + Game1.pixelZoom + ); + + _downArrow = new ClickableTextureComponent( + new Rectangle( + _upArrow.bounds.X, + yPositionOnScreen + height - Game1.tileSize, + _upArrow.bounds.Width, + _upArrow.bounds.Height + ), + Game1.mouseCursors, + new Rectangle(421, 472, 11, 12), + Game1.pixelZoom + ); + + _scrollBar = new ClickableTextureComponent( + new Rectangle( + _upArrow.bounds.X + Game1.pixelZoom * 3, + _upArrow.bounds.Y + _upArrow.bounds.Height + Game1.pixelZoom, + 6 * Game1.pixelZoom, + 10 * Game1.pixelZoom + ), + Game1.mouseCursors, + new Rectangle(435, 463, 6, 10), + Game1.pixelZoom + ); + + _scrollBarRunner = new Rectangle( + _scrollBar.bounds.X, + _scrollBar.bounds.Y, + _scrollBar.bounds.Width, + height - Game1.tileSize * 2 - _upArrow.bounds.Height - Game1.pixelZoom * 2 + ); + + for (var i = 0; i < visibleSlots; ++i) + { + // tqdv: I'm not sure where Game1.tileSize and Game1.pixelZoom come from + var component = new ClickableComponent( + new Rectangle( + xPositionOnScreen + Game1.tileSize / 4, + yPositionOnScreen + + Game1.tileSize * 5 / 4 + + Game1.pixelZoom + + i * (height - Game1.tileSize * 2) / visibleSlots, + width - Game1.tileSize / 2, + (height - Game1.tileSize * 2) / visibleSlots + Game1.pixelZoom + ), + i.ToString() + ) { - _options = options; - _upArrow = new ClickableTextureComponent( - new Rectangle( - xPositionOnScreen + width + Game1.tileSize / 4, - yPositionOnScreen + Game1.tileSize, - 11 * Game1.pixelZoom, - 12 * Game1.pixelZoom), - Game1.mouseCursors, - new Rectangle(421, 459, 11, 12), - Game1.pixelZoom); - - _downArrow = new ClickableTextureComponent( - new Rectangle( - _upArrow.bounds.X, - yPositionOnScreen + height - Game1.tileSize, - _upArrow.bounds.Width, - _upArrow.bounds.Height), - Game1.mouseCursors, - new Rectangle(421, 472, 11, 12), - Game1.pixelZoom); - - _scrollBar = new ClickableTextureComponent( - new Rectangle( - _upArrow.bounds.X + Game1.pixelZoom * 3, - _upArrow.bounds.Y + _upArrow.bounds.Height + Game1.pixelZoom, - 6 * Game1.pixelZoom, - 10 * Game1.pixelZoom), - Game1.mouseCursors, - new Rectangle(435, 463, 6, 10), - Game1.pixelZoom); - - _scrollBarRunner = new Rectangle(_scrollBar.bounds.X, - _scrollBar.bounds.Y, - _scrollBar.bounds.Width, - height - Game1.tileSize * 2 - _upArrow.bounds.Height - Game1.pixelZoom * 2); - - for (int i = 0; i < visibleSlots; ++i) - { - // tqdv: I'm not sure where Game1.tileSize and Game1.pixelZoom come from - var component = new ClickableComponent( - new Rectangle( - xPositionOnScreen + Game1.tileSize / 4, - yPositionOnScreen + Game1.tileSize * 5 / 4 + Game1.pixelZoom + i * (height - Game1.tileSize * 2) / visibleSlots, - width - Game1.tileSize / 2, - (height - Game1.tileSize * 2) / visibleSlots + Game1.pixelZoom), - i.ToString()) - { - myID = i, - downNeighborID = (i+1 < visibleSlots ? i+1 : ClickableComponent.CUSTOM_SNAP_BEHAVIOR), - upNeighborID = (i-1 >= 0 ? i-1 : ClickableComponent.CUSTOM_SNAP_BEHAVIOR), - fullyImmutable = true - }; - _optionSlots.Add(component); - } - - events.Display.MenuChanged += OnMenuChanged; - } + myID = i, + downNeighborID = i + 1 < visibleSlots ? i + 1 : ClickableComponent.CUSTOM_SNAP_BEHAVIOR, + upNeighborID = i - 1 >= 0 ? i - 1 : ClickableComponent.CUSTOM_SNAP_BEHAVIOR, + fullyImmutable = true + }; + _optionSlots.Add(component); + } + + events.Display.MenuChanged += OnMenuChanged; + } - /// Raised after a game menu is opened, closed, or replaced. - /// The event sender. - /// The event arguments. - private void OnMenuChanged(object sender, MenuChangedEventArgs e) + /// Raised after a game menu is opened, closed, or replaced. + /// The event sender. + /// The event arguments. + private void OnMenuChanged(object sender, MenuChangedEventArgs e) + { + if (e.NewMenu is GameMenu) + { + xPositionOnScreen = Game1.activeClickableMenu.xPositionOnScreen; + yPositionOnScreen = Game1.activeClickableMenu.yPositionOnScreen + 10; + height = Game1.activeClickableMenu.height; + + for (var i = 0; i < _optionSlots.Count; ++i) { - if (e.NewMenu is GameMenu) - { - xPositionOnScreen = Game1.activeClickableMenu.xPositionOnScreen; - yPositionOnScreen = Game1.activeClickableMenu.yPositionOnScreen + 10; - height = Game1.activeClickableMenu.height; - - for (int i = 0; i < _optionSlots.Count; ++i) - { - var next = _optionSlots[i]; - next.bounds.X = xPositionOnScreen + Game1.tileSize / 4; - next.bounds.Y = yPositionOnScreen + Game1.tileSize * 5 / 4 + Game1.pixelZoom + i * (height - Game1.tileSize * 2) / 7; - next.bounds.Width = width - Game1.tileSize / 2; - next.bounds.Height = (height - Game1.tileSize * 2) / 7 + Game1.pixelZoom; - } - - _upArrow.bounds.X = xPositionOnScreen + width + Game1.tileSize / 4; - _upArrow.bounds.Y = yPositionOnScreen + Game1.tileSize; - _upArrow.bounds.Width = 11 * Game1.pixelZoom; - _upArrow.bounds.Height = 12 * Game1.pixelZoom; - - _downArrow.bounds.X = _upArrow.bounds.X; - _downArrow.bounds.Y = yPositionOnScreen + height - Game1.tileSize; - _downArrow.bounds.Width = _upArrow.bounds.Width; - _downArrow.bounds.Height = _upArrow.bounds.Height; - - _scrollBar.bounds.X = _upArrow.bounds.X + Game1.pixelZoom * 3; - _scrollBar.bounds.Y = _upArrow.bounds.Y + _upArrow.bounds.Height + Game1.pixelZoom; - _scrollBar.bounds.Width = 6 * Game1.pixelZoom; - _scrollBar.bounds.Height = 10 * Game1.pixelZoom; - - _scrollBarRunner.X = _scrollBar.bounds.X; - _scrollBarRunner.Y = _scrollBar.bounds.Y; - _scrollBarRunner.Width = _scrollBar.bounds.Width; - _scrollBarRunner.Height = height - Game1.tileSize * 2 - _upArrow.bounds.Height - Game1.pixelZoom * 2; - } + ClickableComponent next = _optionSlots[i]; + next.bounds.X = xPositionOnScreen + Game1.tileSize / 4; + next.bounds.Y = yPositionOnScreen + + Game1.tileSize * 5 / 4 + + Game1.pixelZoom + + i * (height - Game1.tileSize * 2) / 7; + next.bounds.Width = width - Game1.tileSize / 2; + next.bounds.Height = (height - Game1.tileSize * 2) / 7 + Game1.pixelZoom; } - public override void snapToDefaultClickableComponent() + _upArrow.bounds.X = xPositionOnScreen + width + Game1.tileSize / 4; + _upArrow.bounds.Y = yPositionOnScreen + Game1.tileSize; + _upArrow.bounds.Width = 11 * Game1.pixelZoom; + _upArrow.bounds.Height = 12 * Game1.pixelZoom; + + _downArrow.bounds.X = _upArrow.bounds.X; + _downArrow.bounds.Y = yPositionOnScreen + height - Game1.tileSize; + _downArrow.bounds.Width = _upArrow.bounds.Width; + _downArrow.bounds.Height = _upArrow.bounds.Height; + + _scrollBar.bounds.X = _upArrow.bounds.X + Game1.pixelZoom * 3; + _scrollBar.bounds.Y = _upArrow.bounds.Y + _upArrow.bounds.Height + Game1.pixelZoom; + _scrollBar.bounds.Width = 6 * Game1.pixelZoom; + _scrollBar.bounds.Height = 10 * Game1.pixelZoom; + + _scrollBarRunner.X = _scrollBar.bounds.X; + _scrollBarRunner.Y = _scrollBar.bounds.Y; + _scrollBarRunner.Width = _scrollBar.bounds.Width; + _scrollBarRunner.Height = height - Game1.tileSize * 2 - _upArrow.bounds.Height - Game1.pixelZoom * 2; + } + } + + public override void snapToDefaultClickableComponent() + { + currentlySnappedComponent = getComponentWithID(1); + snapCursorToCurrentSnappedComponent(); + } + + protected override void customSnapBehavior(int direction, int oldRegion, int oldID) + { + if (oldID == visibleSlots - 1 && direction == Game1.down) + { + if (_currentItemIndex + visibleSlots < _options.Count) { - base.currentlySnappedComponent = base.getComponentWithID(1); - this.snapCursorToCurrentSnappedComponent(); + DownArrowPressed(); + Game1.playSound("shiny4"); } - - protected override void customSnapBehavior(int direction, int oldRegion, int oldID) + } + else if (oldID == 0 && direction == Game1.up) + { + if (_currentItemIndex > 0) { - if (oldID == visibleSlots-1 && direction == Game1.down) - { - if (_currentItemIndex + visibleSlots < _options.Count) - { - DownArrowPressed(); - Game1.playSound("shiny4"); - } - } - else if (oldID == 0 && direction == Game1.up) - { - if (_currentItemIndex > 0) - { - UpArrowPressed(); - Game1.playSound("shiny4"); - } - else - { - // Already at the top, move to the menu tab - base.currentlySnappedComponent = base.getComponentWithID(12348); - if (base.currentlySnappedComponent != null) - { - // Set the down neighbor of the tab to the first slot, instead of the default (which is the second slot) - base.currentlySnappedComponent.downNeighborID = 0; - } - snapCursorToCurrentSnappedComponent(); - } - } + UpArrowPressed(); + Game1.playSound("shiny4"); } - - public override void snapCursorToCurrentSnappedComponent() + else { - if (base.currentlySnappedComponent != null) - { - var snappedElement = GetVisibleOption(base.currentlySnappedComponent.myID); - if (snappedElement != null) - { - var maybePos = snappedElement.GetRelativeSnapPoint(base.currentlySnappedComponent.bounds); - if (maybePos is Point pos) // if it's not null - { - Game1.setMousePosition( - base.currentlySnappedComponent.bounds.X + pos.X, - base.currentlySnappedComponent.bounds.Y + pos.Y); - return; - } - } - - if (base.currentlySnappedComponent.myID < visibleSlots) - { - ModEntry.MonitorObject.Log($"{this.GetType().Name}: Using default snap position for a slot"); - - // Positioning taken from OptionsPage.snapCursorToCurrentSnappedComponent - Game1.setMousePosition(base.currentlySnappedComponent.bounds.Left + 48, base.currentlySnappedComponent.bounds.Center.Y - 12); - } - else - { - base.snapCursorToCurrentSnappedComponent(); - } - } + // Already at the top, move to the menu tab + currentlySnappedComponent = getComponentWithID(12348); + if (currentlySnappedComponent != null) + { + // Set the down neighbor of the tab to the first slot, instead of the default (which is the second slot) + currentlySnappedComponent.downNeighborID = 0; + } + + snapCursorToCurrentSnappedComponent(); } + } + } - private void SetScrollBarToCurrentItem() + public override void snapCursorToCurrentSnappedComponent() + { + if (currentlySnappedComponent != null) + { + ModOptionsElement? snappedElement = GetVisibleOption(currentlySnappedComponent.myID); + if (snappedElement != null) { - if (_options.Count > 0) - { - _scrollBar.bounds.Y = _scrollBarRunner.Height / Math.Max(1, _options.Count - 7 + 1) * _currentItemIndex + _upArrow.bounds.Bottom + Game1.pixelZoom; - - if (_currentItemIndex == _options.Count - 7) - { - _scrollBar.bounds.Y = _downArrow.bounds.Y - _scrollBar.bounds.Height - Game1.pixelZoom; - } - } + Point? maybePos = snappedElement.GetRelativeSnapPoint(currentlySnappedComponent.bounds); + if (maybePos is Point pos) // if it's not null + { + Game1.setMousePosition( + currentlySnappedComponent.bounds.X + pos.X, + currentlySnappedComponent.bounds.Y + pos.Y + ); + return; + } } - public override void leftClickHeld(int x, int y) + if (currentlySnappedComponent.myID < visibleSlots) { - if (!GameMenu.forcePreventClose) - { - base.leftClickHeld(x, y); - - if (_isScrolling) - { - int yBefore = _scrollBar.bounds.Y; - - _scrollBar.bounds.Y = Math.Min( - yPositionOnScreen + height - Game1.tileSize - Game1.pixelZoom * 3 - _scrollBar.bounds.Height, - Math.Max( - y, - yPositionOnScreen + _upArrow.bounds.Height + Game1.pixelZoom * 5)); - - _currentItemIndex = Math.Max(0, - Math.Min(_options.Count - visibleSlots, - _options.Count * (y - _scrollBarRunner.Y) / _scrollBarRunner.Height)); - - SetScrollBarToCurrentItem(); - - if (yBefore != _scrollBar.bounds.Y) - Game1.playSound("shiny4"); - } - else if (_optionsSlotHeld > -1 && _optionsSlotHeld + _currentItemIndex < _options.Count) - { - _options[_currentItemIndex + _optionsSlotHeld].LeftClickHeld( - x - _optionSlots[_optionsSlotHeld].bounds.X, - y - _optionSlots[_optionsSlotHeld].bounds.Y); - } - } - } + ModEntry.MonitorObject.Log($"{GetType().Name}: Using default snap position for a slot"); - public override void receiveKeyPress(Keys key) + // Positioning taken from OptionsPage.snapCursorToCurrentSnappedComponent + Game1.setMousePosition( + currentlySnappedComponent.bounds.Left + 48, + currentlySnappedComponent.bounds.Center.Y - 12 + ); + } + else { - if (_optionsSlotHeld > -1 && _optionsSlotHeld + _currentItemIndex < _options.Count) - { - _options[_currentItemIndex + _optionsSlotHeld].ReceiveKeyPress(key); - } - else - { - // The base implementation handles gamepad movement - base.receiveKeyPress(key); - } + base.snapCursorToCurrentSnappedComponent(); } + } + } + + private void SetScrollBarToCurrentItem() + { + if (_options.Count > 0) + { + _scrollBar.bounds.Y = _scrollBarRunner.Height / Math.Max(1, _options.Count - 7 + 1) * _currentItemIndex + + _upArrow.bounds.Bottom + + Game1.pixelZoom; - public override void receiveScrollWheelAction(int direction) + if (_currentItemIndex == _options.Count - 7) { - if (!GameMenu.forcePreventClose) - { - base.receiveScrollWheelAction(direction); - - if (direction > 0 && _currentItemIndex > 0) - { - UpArrowPressed(); - Game1.playSound("shiny4"); - } - else if (direction < 0 && _currentItemIndex + visibleSlots < _options.Count) - { - DownArrowPressed(); - Game1.playSound("shiny4"); - } - } + _scrollBar.bounds.Y = _downArrow.bounds.Y - _scrollBar.bounds.Height - Game1.pixelZoom; } + } + } - public override void releaseLeftClick(int x, int y) + public override void leftClickHeld(int x, int y) + { + if (!GameMenu.forcePreventClose) + { + base.leftClickHeld(x, y); + + if (_isScrolling) { - if (!GameMenu.forcePreventClose) - { - base.releaseLeftClick(x, y); - - if (_optionsSlotHeld > -1 && _optionsSlotHeld + _currentItemIndex < _options.Count) - { - ClickableComponent optionSlot = _optionSlots[_optionsSlotHeld]; - _options[_currentItemIndex + _optionsSlotHeld].LeftClickReleased(x - optionSlot.bounds.X, y - optionSlot.bounds.Y); - } - _optionsSlotHeld = -1; - _isScrolling = false; - } - } + int yBefore = _scrollBar.bounds.Y; + + _scrollBar.bounds.Y = Math.Min( + yPositionOnScreen + height - Game1.tileSize - Game1.pixelZoom * 3 - _scrollBar.bounds.Height, + Math.Max(y, yPositionOnScreen + _upArrow.bounds.Height + Game1.pixelZoom * 5) + ); + + _currentItemIndex = Math.Max( + 0, + Math.Min(_options.Count - visibleSlots, _options.Count * (y - _scrollBarRunner.Y) / _scrollBarRunner.Height) + ); - private void DownArrowPressed() + SetScrollBarToCurrentItem(); + + if (yBefore != _scrollBar.bounds.Y) + { + Game1.playSound("shiny4"); + } + } + else if (_optionsSlotHeld > -1 && _optionsSlotHeld + _currentItemIndex < _options.Count) { - _downArrow.scale = _downArrow.baseScale; - ++_currentItemIndex; - SetScrollBarToCurrentItem(); + _options[_currentItemIndex + _optionsSlotHeld] + .LeftClickHeld(x - _optionSlots[_optionsSlotHeld].bounds.X, y - _optionSlots[_optionsSlotHeld].bounds.Y); } + } + } - private void UpArrowPressed() + public override void receiveKeyPress(Keys key) + { + if (_optionsSlotHeld > -1 && _optionsSlotHeld + _currentItemIndex < _options.Count) + { + _options[_currentItemIndex + _optionsSlotHeld].ReceiveKeyPress(key); + } + else + { + // The base implementation handles gamepad movement + base.receiveKeyPress(key); + } + } + + public override void receiveScrollWheelAction(int direction) + { + if (!GameMenu.forcePreventClose) + { + base.receiveScrollWheelAction(direction); + + if (direction > 0 && _currentItemIndex > 0) { - _upArrow.scale = _upArrow.baseScale; - --_currentItemIndex; - SetScrollBarToCurrentItem(); + UpArrowPressed(); + Game1.playSound("shiny4"); } - - public override void receiveLeftClick(int x, int y, bool playSound = true) + else if (direction < 0 && _currentItemIndex + visibleSlots < _options.Count) { - if (!GameMenu.forcePreventClose) - { - if (_downArrow.containsPoint(x, y) && _currentItemIndex < Math.Max(0, _options.Count - 7)) - { - DownArrowPressed(); - Game1.playSound("shwip"); - } - else if (_upArrow.containsPoint(x, y) && _currentItemIndex > 0) - { - UpArrowPressed(); - Game1.playSound("shwip"); - } - else if (_scrollBar.containsPoint(x, y)) - { - _isScrolling = true; - } - else if (!_downArrow.containsPoint(x, y) && - x > xPositionOnScreen + width && - x < xPositionOnScreen + width + Game1.tileSize * 2 && - y > yPositionOnScreen && - y < yPositionOnScreen + height) - { - // Handle scrollbar click even if the player clicked right next to it, but do not enable scrollbar dragging - // NB the leniency area is based on the option page's, so it's too large - _isScrolling = true; - base.leftClickHeld(x, y); - base.releaseLeftClick(x, y); - } - _currentItemIndex = Math.Max(0, Math.Min(_options.Count - visibleSlots, _currentItemIndex)); - for (int i = 0; i < _optionSlots.Count; ++i) - { - if (_optionSlots[i].bounds.Contains(x, y) && - _currentItemIndex + i < _options.Count && - _options[_currentItemIndex + i].Bounds.Contains(x - _optionSlots[i].bounds.X, y - _optionSlots[i].bounds.Y)) - { - _options[_currentItemIndex + i].ReceiveLeftClick( - x - _optionSlots[i].bounds.X, - y - _optionSlots[i].bounds.Y); - _optionsSlotHeld = i; - break; - } - } - } + DownArrowPressed(); + Game1.playSound("shiny4"); } + } + } + public override void releaseLeftClick(int x, int y) + { + if (!GameMenu.forcePreventClose) + { + base.releaseLeftClick(x, y); - public override void receiveRightClick(int x, int y, bool playSound = true) + if (_optionsSlotHeld > -1 && _optionsSlotHeld + _currentItemIndex < _options.Count) { - + ClickableComponent optionSlot = _optionSlots[_optionsSlotHeld]; + _options[_currentItemIndex + _optionsSlotHeld] + .LeftClickReleased(x - optionSlot.bounds.X, y - optionSlot.bounds.Y); } - public override void performHoverAction(int x, int y) + _optionsSlotHeld = -1; + _isScrolling = false; + } + } + + private void DownArrowPressed() + { + _downArrow.scale = _downArrow.baseScale; + ++_currentItemIndex; + SetScrollBarToCurrentItem(); + } + + private void UpArrowPressed() + { + _upArrow.scale = _upArrow.baseScale; + --_currentItemIndex; + SetScrollBarToCurrentItem(); + } + + public override void receiveLeftClick(int x, int y, bool playSound = true) + { + if (!GameMenu.forcePreventClose) + { + if (_downArrow.containsPoint(x, y) && _currentItemIndex < Math.Max(0, _options.Count - 7)) { - if (!GameMenu.forcePreventClose) - { - _hoverText = ""; - _upArrow.tryHover(x, y); - _downArrow.tryHover(x, y); - _scrollBar.tryHover(x, y); - } + DownArrowPressed(); + Game1.playSound("shwip"); } - - public override void draw(SpriteBatch batch) + else if (_upArrow.containsPoint(x, y) && _currentItemIndex > 0) { - Game1.drawDialogueBox(xPositionOnScreen, yPositionOnScreen - 10, width, height, false, true); - batch.End(); - batch.Begin(SpriteSortMode.FrontToBack, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null); - for (int i = 0; i < _optionSlots.Count; ++i) - { - if (_currentItemIndex >= 0 && - _currentItemIndex + i < _options.Count) - { - _options[_currentItemIndex + i].Draw( - batch, - _optionSlots[i].bounds.X, - _optionSlots[i].bounds.Y); - } - } - batch.End(); - batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - if (!GameMenu.forcePreventClose) - { - _upArrow.draw(batch); - _downArrow.draw(batch); - if (_options.Count > 7) - { - IClickableMenu.drawTextureBox( - batch, - Game1.mouseCursors, - new Rectangle(403, 383, 6, 6), - _scrollBarRunner.X, - _scrollBarRunner.Y, - _scrollBarRunner.Width, - _scrollBarRunner.Height, - Color.White, - Game1.pixelZoom, - false); - _scrollBar.draw(batch); - } - } - if (_hoverText != "") - IClickableMenu.drawHoverText(batch, _hoverText, Game1.smallFont); + UpArrowPressed(); + Game1.playSound("shwip"); + } + else if (_scrollBar.containsPoint(x, y)) + { + _isScrolling = true; + } + else if (!_downArrow.containsPoint(x, y) && + x > xPositionOnScreen + width && + x < xPositionOnScreen + width + Game1.tileSize * 2 && + y > yPositionOnScreen && + y < yPositionOnScreen + height) + { + // Handle scrollbar click even if the player clicked right next to it, but do not enable scrollbar dragging + // NB the leniency area is based on the option page's, so it's too large + _isScrolling = true; + base.leftClickHeld(x, y); + base.releaseLeftClick(x, y); } - /// Returns the that corresponds to the component ID - /// the mod options element, or null if it is invalid - private ModOptionsElement? GetVisibleOption(int componentId) + _currentItemIndex = Math.Max(0, Math.Min(_options.Count - visibleSlots, _currentItemIndex)); + for (var i = 0; i < _optionSlots.Count; ++i) { - if (componentId >= visibleSlots) - return null; - - int index = _currentItemIndex + componentId; - if (0 <= index && index < _options.Count) - { - return _options[index]; - } - else - { - return null; - } + if (_optionSlots[i].bounds.Contains(x, y) && + _currentItemIndex + i < _options.Count && + _options[_currentItemIndex + i] + .Bounds.Contains(x - _optionSlots[i].bounds.X, y - _optionSlots[i].bounds.Y)) + { + _options[_currentItemIndex + i] + .ReceiveLeftClick(x - _optionSlots[i].bounds.X, y - _optionSlots[i].bounds.Y); + _optionsSlotHeld = i; + break; + } } + } + } - internal void SaveState(ModOptionsPageState state) + + public override void receiveRightClick(int x, int y, bool playSound = true) { } + + public override void performHoverAction(int x, int y) + { + if (!GameMenu.forcePreventClose) + { + _hoverText = ""; + _upArrow.tryHover(x, y); + _downArrow.tryHover(x, y); + _scrollBar.tryHover(x, y); + } + } + + public override void draw(SpriteBatch batch) + { + Game1.drawDialogueBox(xPositionOnScreen, yPositionOnScreen - 10, width, height, false, true); + batch.End(); + batch.Begin(SpriteSortMode.FrontToBack, BlendState.NonPremultiplied, SamplerState.PointClamp); + for (var i = 0; i < _optionSlots.Count; ++i) + { + if (_currentItemIndex >= 0 && _currentItemIndex + i < _options.Count) + { + _options[_currentItemIndex + i].Draw(batch, _optionSlots[i].bounds.X, _optionSlots[i].bounds.Y); + } + } + + batch.End(); + batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp); + if (!GameMenu.forcePreventClose) + { + _upArrow.draw(batch); + _downArrow.draw(batch); + if (_options.Count > 7) { - state.currentIndex = _currentItemIndex; - state.currentComponent = base.currentlySnappedComponent?.myID; + drawTextureBox( + batch, + Game1.mouseCursors, + new Rectangle(403, 383, 6, 6), + _scrollBarRunner.X, + _scrollBarRunner.Y, + _scrollBarRunner.Width, + _scrollBarRunner.Height, + Color.White, + Game1.pixelZoom, + false + ); + _scrollBar.draw(batch); } + } - internal void LoadState(ModOptionsPageState state) + if (_hoverText != "") + { + drawHoverText(batch, _hoverText, Game1.smallFont); + } + } + + /// Returns the that corresponds to the component ID + /// the mod options element, or null if it is invalid + private ModOptionsElement? GetVisibleOption(int componentId) + { + if (componentId >= visibleSlots) + { + return null; + } + + int index = _currentItemIndex + componentId; + if (0 <= index && index < _options.Count) + { + return _options[index]; + } + + return null; + } + + internal void SaveState(ModOptionsPageState state) + { + state.currentIndex = _currentItemIndex; + state.currentComponent = currentlySnappedComponent?.myID; + } + + internal void LoadState(ModOptionsPageState state) + { + if (state.currentIndex is int index) + { + _currentItemIndex = index; + } + + if (state.currentComponent is int componentID) + { + ClickableComponent? component = getComponentWithID(componentID); + if (component != null) { - if (state.currentIndex is int index) - { - _currentItemIndex = index; - } - if (state.currentComponent is int componentID) - { - var component = base.getComponentWithID(componentID); - if (component != null) - { - base.currentlySnappedComponent = component; - this.snapCursorToCurrentSnappedComponent(); - } - } + currentlySnappedComponent = component; + snapCursorToCurrentSnappedComponent(); } + } } + } } diff --git a/UIInfoSuite2/Options/ModOptionsPageButton.cs b/UIInfoSuite2/Options/ModOptionsPageButton.cs index 8a5e8faf..41289abf 100644 --- a/UIInfoSuite2/Options/ModOptionsPageButton.cs +++ b/UIInfoSuite2/Options/ModOptionsPageButton.cs @@ -4,32 +4,36 @@ namespace UIInfoSuite2.Options { - internal class ModOptionsPageButton - { - public int xPositionOnScreen; - public int yPositionOnScreen; + internal class ModOptionsPageButton + { + public int xPositionOnScreen; + public int yPositionOnScreen; - public void draw(SpriteBatch b) - { - Game1.spriteBatch.Draw(Game1.mouseCursors, - new Vector2(xPositionOnScreen, yPositionOnScreen), - new Rectangle(16, 368, 16, 16), - Color.White, - 0.0f, - Vector2.Zero, - Game1.pixelZoom, - SpriteEffects.None, - 1f); + public void draw(SpriteBatch b) + { + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(xPositionOnScreen, yPositionOnScreen), + new Rectangle(16, 368, 16, 16), + Color.White, + 0.0f, + Vector2.Zero, + Game1.pixelZoom, + SpriteEffects.None, + 1f + ); - b.Draw(Game1.mouseCursors, - new Vector2(xPositionOnScreen + 8, yPositionOnScreen + 14), - new Rectangle(32, 672, 16, 16), - Color.White, - 0.0f, - Vector2.Zero, - 3f, - SpriteEffects.None, - 1f); - } + b.Draw( + Game1.mouseCursors, + new Vector2(xPositionOnScreen + 8, yPositionOnScreen + 14), + new Rectangle(32, 672, 16, 16), + Color.White, + 0.0f, + Vector2.Zero, + 3f, + SpriteEffects.None, + 1f + ); } + } } diff --git a/UIInfoSuite2/Options/ModOptionsPageHandler.cs b/UIInfoSuite2/Options/ModOptionsPageHandler.cs index cd750609..eca91e54 100644 --- a/UIInfoSuite2/Options/ModOptionsPageHandler.cs +++ b/UIInfoSuite2/Options/ModOptionsPageHandler.cs @@ -1,499 +1,766 @@ -using StardewModdingAPI; +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework; +using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; -using System; -using System.Collections.Generic; -using System.Reflection; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; using UIInfoSuite2.UIElements; namespace UIInfoSuite2.Options { + /// + /// A mix between working around to add our mod options page, + /// and loading the UI elements with their logic. + /// + internal class ModOptionsPageHandler : IDisposable + { + // Our mod options tab is positioned approximately above the 11th inventory cell + private const int downNeighborInInventory = 10; + private const string optionsTabName = "uiinfosuite2"; + + // For the map page workaround + private readonly PerScreen _changeToOurTabAfterTick = new(); + private readonly List _elementsToDispose; + + private readonly IModHelper _helper; + + // For the window resize workaround + private readonly List _instancesWithOptionsPageOpen = new(); + private readonly PerScreen _lastMenu = new(); + + private readonly PerScreen _lastMenuTab = new(); + + /// The mod options page added to . + private readonly PerScreen _modOptionsPage = new(); + + private readonly PerScreen _modOptionsPageButton = new(); + /// - /// A mix between working around to add our mod options page, - /// and loading the UI elements with their logic. + /// The clickable component for the mod options tab used by gamepad navigation. + /// We don't add it to because it messes up the game's logic. /// - internal class ModOptionsPageHandler : IDisposable + private readonly PerScreen _modOptionsTab = new(); + + private readonly PerScreen _modOptionsTabPageNumber = new(); + + private readonly List _optionsElements = new(); + private readonly PerScreen _savedPageState = new(); + private readonly bool _showPersonalConfigButton; + + private bool _addOurTabBeforeTick; + private bool _windowResizing; + + public ModOptionsPageHandler(IModHelper helper, ModOptions options, bool showPersonalConfigButton) { - // Our mod options tab is positioned approximately above the 11th inventory cell - private const int downNeighborInInventory = 10; - private const string optionsTabName = "uiinfosuite2"; - - private readonly IModHelper _helper; - private readonly bool _showPersonalConfigButton; - - private List _optionsElements = new(); - private readonly List _elementsToDispose; - - /// The mod options page added to . - private PerScreen _modOptionsPage = new(); - /// The clickable component for the mod options tab used by gamepad navigation. - /// We don't add it to because it messes up the game's logic. - private PerScreen _modOptionsTab = new(); - private PerScreen _modOptionsPageButton = new(); - private PerScreen _modOptionsTabPageNumber = new(); - - private PerScreen _lastMenuTab = new(); - private PerScreen _lastMenu = new(); - - // For the window resize workaround - private List _instancesWithOptionsPageOpen = new(); - private bool _windowResizing = false; - private bool _addOurTabBeforeTick = false; - private PerScreen _savedPageState = new(); - // For the map page workaround - private PerScreen _changeToOurTabAfterTick = new(); - - public ModOptionsPageHandler(IModHelper helper, ModOptions options, bool showPersonalConfigButton) - { - if (showPersonalConfigButton) - { - helper.Events.Input.ButtonPressed += OnButtonPressed; - helper.Events.GameLoop.UpdateTicking += OnUpdateTicking; - helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - helper.Events.Display.RenderingActiveMenu += OnRenderingMenu; - helper.Events.Display.RenderedActiveMenu += OnRenderedMenu; - GameRunner.instance.Window.ClientSizeChanged += OnWindowClientSizeChanged; - helper.Events.Display.WindowResized += OnWindowResized; - } - _helper = helper; - _showPersonalConfigButton = showPersonalConfigButton; - - var luckOfDay = new LuckOfDay(helper); - var showBirthdayIcon = new ShowBirthdayIcon(helper); - var showAccurateHearts = new ShowAccurateHearts(helper.Events); - var locationOfTownsfolk = new LocationOfTownsfolk(helper, options); - var showWhenAnimalNeedsPet = new ShowWhenAnimalNeedsPet(helper); - var showCalendarAndBillboardOnGameMenuButton = new ShowCalendarAndBillboardOnGameMenuButton(helper); - var showScarecrowAndSprinklerRange = new ShowItemEffectRanges(helper); - var experienceBar = new ExperienceBar(helper); - var showItemHoverInformation = new ShowItemHoverInformation(helper); - var shopHarvestPrices = new ShopHarvestPrices(helper); - var showQueenOfSauceIcon = new ShowQueenOfSauceIcon(helper); - var showTravelingMerchant = new ShowTravelingMerchant(helper); - var showRainyDayIcon = new ShowRainyDayIcon(helper); - var showCropAndBarrelTime = new ShowCropAndBarrelTime(helper); - var showToolUpgradeStatus = new ShowToolUpgradeStatus(helper); - var showRobinBuildingStatusIcon = new ShowRobinBuildingStatusIcon(helper); - var showSeasonalBerry = new ShowSeasonalBerry(helper); - var showTodaysGift = new ShowTodaysGifts(helper); - - _elementsToDispose = new List() - { - luckOfDay, - showBirthdayIcon, - showAccurateHearts, - locationOfTownsfolk, - showWhenAnimalNeedsPet, - showCalendarAndBillboardOnGameMenuButton, - showCropAndBarrelTime, - experienceBar, - showItemHoverInformation, - showTravelingMerchant, - showRainyDayIcon, - shopHarvestPrices, - showQueenOfSauceIcon, - showToolUpgradeStatus, - showRobinBuildingStatusIcon, - showSeasonalBerry - }; - - int whichOption = 1; - _optionsElements.Add(new ModOptionsElement($"UI Info Suite 2 {GetVersionString(helper)}")); - - var luckIcon = new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowLuckIcon)), whichOption++, luckOfDay.ToggleOption, () => options.ShowLuckIcon, v => options.ShowLuckIcon = v); - _optionsElements.Add(luckIcon); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowExactValue)), whichOption++, luckOfDay.ToggleShowExactValueOption, () => options.ShowExactValue, v => options.ShowExactValue = v, luckIcon)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowLevelUpAnimation)), whichOption++, experienceBar.ToggleLevelUpAnimation, () => options.ShowLevelUpAnimation, v => options.ShowLevelUpAnimation = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowExperienceBar)), whichOption++, experienceBar.ToggleShowExperienceBar, () => options.ShowExperienceBar, v => options.ShowExperienceBar = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.AllowExperienceBarToFadeOut)), whichOption++, experienceBar.ToggleExperienceBarFade, () => options.AllowExperienceBarToFadeOut, v => options.AllowExperienceBarToFadeOut = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowExperienceGain)), whichOption++, experienceBar.ToggleShowExperienceGain, () => options.ShowExperienceGain, v => options.ShowExperienceGain = v)); - if (!_helper.ModRegistry.IsLoaded("Bouhm.NPCMapLocations")) - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowLocationOfTownsPeople)), whichOption++, locationOfTownsfolk.ToggleShowNPCLocationsOnMap, () => options.ShowLocationOfTownsPeople, v => options.ShowLocationOfTownsPeople = v)); - var birthdayIcon = new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowBirthdayIcon)), whichOption++, showBirthdayIcon.ToggleOption, () => options.ShowBirthdayIcon, v => options.ShowBirthdayIcon = v); - _optionsElements.Add(birthdayIcon); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.HideBirthdayIfFullFriendShip)), whichOption++, showBirthdayIcon.ToggleDisableOnMaxFriendshipOption, () => options.HideBirthdayIfFullFriendShip, v => options.HideBirthdayIfFullFriendShip = v, birthdayIcon)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowHeartFills)), whichOption++, showAccurateHearts.ToggleOption, () => options.ShowHeartFills, v => options.ShowHeartFills = v)); - var animalPetIcon = new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowAnimalsNeedPets)), whichOption++, showWhenAnimalNeedsPet.ToggleOption, () => options.ShowAnimalsNeedPets, v => options.ShowAnimalsNeedPets = v); - _optionsElements.Add(animalPetIcon); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.HideAnimalPetOnMaxFriendship)), whichOption++, showWhenAnimalNeedsPet.ToggleDisableOnMaxFriendshipOption, () => options.HideAnimalPetOnMaxFriendship, v => options.HideAnimalPetOnMaxFriendship = v, animalPetIcon)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.DisplayCalendarAndBillboard)), whichOption++, showCalendarAndBillboardOnGameMenuButton.ToggleOption, () => options.DisplayCalendarAndBillboard, v => options.DisplayCalendarAndBillboard = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowCropAndBarrelTooltip)), whichOption++, showCropAndBarrelTime.ToggleOption, () => options.ShowCropAndBarrelTooltip, v => options.ShowCropAndBarrelTooltip = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowItemEffectRanges)), whichOption++, showScarecrowAndSprinklerRange.ToggleOption, () => options.ShowItemEffectRanges, v => options.ShowItemEffectRanges = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowExtraItemInformation)), whichOption++, showItemHoverInformation.ToggleOption, () => options.ShowExtraItemInformation, v => options.ShowExtraItemInformation = v)); - var travellingMerchantIcon = new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowTravelingMerchant)), whichOption++, showTravelingMerchant.ToggleOption, () => options.ShowTravelingMerchant, v => options.ShowTravelingMerchant = v); - _optionsElements.Add(travellingMerchantIcon); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.HideMerchantWhenVisited)), whichOption++, showTravelingMerchant.ToggleHideWhenVisitedOption, () => options.HideMerchantWhenVisited, v => options.HideMerchantWhenVisited = v, travellingMerchantIcon)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowRainyDay)), whichOption++, showRainyDayIcon.ToggleOption, () => options.ShowRainyDay, v => options.ShowRainyDay = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowHarvestPricesInShop)), whichOption++, shopHarvestPrices.ToggleOption, () => options.ShowHarvestPricesInShop, v => options.ShowHarvestPricesInShop = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowWhenNewRecipesAreAvailable)), whichOption++, showQueenOfSauceIcon.ToggleOption, () => options.ShowWhenNewRecipesAreAvailable, v => options.ShowWhenNewRecipesAreAvailable = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowToolUpgradeStatus)), whichOption++, showToolUpgradeStatus.ToggleOption, () => options.ShowToolUpgradeStatus, v => options.ShowToolUpgradeStatus = v)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowRobinBuildingStatusIcon)), whichOption++, showRobinBuildingStatusIcon.ToggleOption, () => options.ShowRobinBuildingStatusIcon, v => options.ShowRobinBuildingStatusIcon = v)); - var seasonalBerryIcon = new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowSeasonalBerry)), whichOption++, showSeasonalBerry.ToggleOption, () => options.ShowSeasonalBerry, v => options.ShowSeasonalBerry = v); - _optionsElements.Add(seasonalBerryIcon); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowSeasonalBerryHazelnut)), whichOption++, showSeasonalBerry.ToggleHazelnutOption, () => options.ShowSeasonalBerryHazelnut, v => options.ShowSeasonalBerryHazelnut = v, seasonalBerryIcon)); - _optionsElements.Add(new ModOptionsCheckbox(_helper.SafeGetString(nameof(options.ShowTodaysGifts)), whichOption++, showTodaysGift.ToggleOption, () => options.ShowTodaysGifts, v => options.ShowTodaysGifts = v)); - } + if (showPersonalConfigButton) + { + helper.Events.Input.ButtonPressed += OnButtonPressed; + helper.Events.GameLoop.UpdateTicking += OnUpdateTicking; + helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + helper.Events.Display.RenderingActiveMenu += OnRenderingMenu; + helper.Events.Display.RenderedActiveMenu += OnRenderedMenu; + GameRunner.instance.Window.ClientSizeChanged += OnWindowClientSizeChanged; + helper.Events.Display.WindowResized += OnWindowResized; + } + + _helper = helper; + _showPersonalConfigButton = showPersonalConfigButton; + + var luckOfDay = new LuckOfDay(helper); + var showBirthdayIcon = new ShowBirthdayIcon(helper); + var showAccurateHearts = new ShowAccurateHearts(helper.Events); + var locationOfTownsfolk = new LocationOfTownsfolk(helper, options); + var showWhenAnimalNeedsPet = new ShowWhenAnimalNeedsPet(helper); + var showCalendarAndBillboardOnGameMenuButton = new ShowCalendarAndBillboardOnGameMenuButton(helper); + var showScarecrowAndSprinklerRange = new ShowItemEffectRanges(helper); + var experienceBar = new ExperienceBar(helper); + var showItemHoverInformation = new ShowItemHoverInformation(helper); + var shopHarvestPrices = new ShopHarvestPrices(helper); + var showQueenOfSauceIcon = new ShowQueenOfSauceIcon(helper); + var showTravelingMerchant = new ShowTravelingMerchant(helper); + var showRainyDayIcon = new ShowRainyDayIcon(helper); + var showCropAndBarrelTime = new ShowCropAndBarrelTime(helper); + var showToolUpgradeStatus = new ShowToolUpgradeStatus(helper); + var showRobinBuildingStatusIcon = new ShowRobinBuildingStatusIcon(helper); + var showSeasonalBerry = new ShowSeasonalBerry(helper); + var showTodaysGift = new ShowTodaysGifts(helper); + + _elementsToDispose = new List + { + luckOfDay, + showBirthdayIcon, + showAccurateHearts, + locationOfTownsfolk, + showWhenAnimalNeedsPet, + showCalendarAndBillboardOnGameMenuButton, + showCropAndBarrelTime, + experienceBar, + showItemHoverInformation, + showTravelingMerchant, + showRainyDayIcon, + shopHarvestPrices, + showQueenOfSauceIcon, + showToolUpgradeStatus, + showRobinBuildingStatusIcon, + showSeasonalBerry + }; + + var whichOption = 1; + _optionsElements.Add(new ModOptionsElement($"UI Info Suite 2 {GetVersionString(helper)}")); + + var luckIcon = new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowLuckIcon)), + whichOption++, + luckOfDay.ToggleOption, + () => options.ShowLuckIcon, + v => options.ShowLuckIcon = v + ); + _optionsElements.Add(luckIcon); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowExactValue)), + whichOption++, + luckOfDay.ToggleShowExactValueOption, + () => options.ShowExactValue, + v => options.ShowExactValue = v, + luckIcon + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowLevelUpAnimation)), + whichOption++, + experienceBar.ToggleLevelUpAnimation, + () => options.ShowLevelUpAnimation, + v => options.ShowLevelUpAnimation = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowExperienceBar)), + whichOption++, + experienceBar.ToggleShowExperienceBar, + () => options.ShowExperienceBar, + v => options.ShowExperienceBar = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.AllowExperienceBarToFadeOut)), + whichOption++, + experienceBar.ToggleExperienceBarFade, + () => options.AllowExperienceBarToFadeOut, + v => options.AllowExperienceBarToFadeOut = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowExperienceGain)), + whichOption++, + experienceBar.ToggleShowExperienceGain, + () => options.ShowExperienceGain, + v => options.ShowExperienceGain = v + ) + ); + if (!_helper.ModRegistry.IsLoaded("Bouhm.NPCMapLocations")) + { + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowLocationOfTownsPeople)), + whichOption++, + locationOfTownsfolk.ToggleShowNPCLocationsOnMap, + () => options.ShowLocationOfTownsPeople, + v => options.ShowLocationOfTownsPeople = v + ) + ); + } + + var birthdayIcon = new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowBirthdayIcon)), + whichOption++, + showBirthdayIcon.ToggleOption, + () => options.ShowBirthdayIcon, + v => options.ShowBirthdayIcon = v + ); + _optionsElements.Add(birthdayIcon); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.HideBirthdayIfFullFriendShip)), + whichOption++, + showBirthdayIcon.ToggleDisableOnMaxFriendshipOption, + () => options.HideBirthdayIfFullFriendShip, + v => options.HideBirthdayIfFullFriendShip = v, + birthdayIcon + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowHeartFills)), + whichOption++, + showAccurateHearts.ToggleOption, + () => options.ShowHeartFills, + v => options.ShowHeartFills = v + ) + ); + var animalPetIcon = new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowAnimalsNeedPets)), + whichOption++, + showWhenAnimalNeedsPet.ToggleOption, + () => options.ShowAnimalsNeedPets, + v => options.ShowAnimalsNeedPets = v + ); + _optionsElements.Add(animalPetIcon); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.HideAnimalPetOnMaxFriendship)), + whichOption++, + showWhenAnimalNeedsPet.ToggleDisableOnMaxFriendshipOption, + () => options.HideAnimalPetOnMaxFriendship, + v => options.HideAnimalPetOnMaxFriendship = v, + animalPetIcon + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.DisplayCalendarAndBillboard)), + whichOption++, + showCalendarAndBillboardOnGameMenuButton.ToggleOption, + () => options.DisplayCalendarAndBillboard, + v => options.DisplayCalendarAndBillboard = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowCropAndBarrelTooltip)), + whichOption++, + showCropAndBarrelTime.ToggleOption, + () => options.ShowCropAndBarrelTooltip, + v => options.ShowCropAndBarrelTooltip = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowItemEffectRanges)), + whichOption++, + showScarecrowAndSprinklerRange.ToggleOption, + () => options.ShowItemEffectRanges, + v => options.ShowItemEffectRanges = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowExtraItemInformation)), + whichOption++, + showItemHoverInformation.ToggleOption, + () => options.ShowExtraItemInformation, + v => options.ShowExtraItemInformation = v + ) + ); + var travellingMerchantIcon = new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowTravelingMerchant)), + whichOption++, + showTravelingMerchant.ToggleOption, + () => options.ShowTravelingMerchant, + v => options.ShowTravelingMerchant = v + ); + _optionsElements.Add(travellingMerchantIcon); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.HideMerchantWhenVisited)), + whichOption++, + showTravelingMerchant.ToggleHideWhenVisitedOption, + () => options.HideMerchantWhenVisited, + v => options.HideMerchantWhenVisited = v, + travellingMerchantIcon + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowRainyDay)), + whichOption++, + showRainyDayIcon.ToggleOption, + () => options.ShowRainyDay, + v => options.ShowRainyDay = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowHarvestPricesInShop)), + whichOption++, + shopHarvestPrices.ToggleOption, + () => options.ShowHarvestPricesInShop, + v => options.ShowHarvestPricesInShop = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowWhenNewRecipesAreAvailable)), + whichOption++, + showQueenOfSauceIcon.ToggleOption, + () => options.ShowWhenNewRecipesAreAvailable, + v => options.ShowWhenNewRecipesAreAvailable = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowToolUpgradeStatus)), + whichOption++, + showToolUpgradeStatus.ToggleOption, + () => options.ShowToolUpgradeStatus, + v => options.ShowToolUpgradeStatus = v + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowRobinBuildingStatusIcon)), + whichOption++, + showRobinBuildingStatusIcon.ToggleOption, + () => options.ShowRobinBuildingStatusIcon, + v => options.ShowRobinBuildingStatusIcon = v + ) + ); + var seasonalBerryIcon = new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowSeasonalBerry)), + whichOption++, + showSeasonalBerry.ToggleOption, + () => options.ShowSeasonalBerry, + v => options.ShowSeasonalBerry = v + ); + _optionsElements.Add(seasonalBerryIcon); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowSeasonalBerryHazelnut)), + whichOption++, + showSeasonalBerry.ToggleHazelnutOption, + () => options.ShowSeasonalBerryHazelnut, + v => options.ShowSeasonalBerryHazelnut = v, + seasonalBerryIcon + ) + ); + _optionsElements.Add( + new ModOptionsCheckbox( + _helper.SafeGetString(nameof(options.ShowTodaysGifts)), + whichOption++, + showTodaysGift.ToggleOption, + () => options.ShowTodaysGifts, + v => options.ShowTodaysGifts = v + ) + ); + } - public void Dispose() - { - foreach (var item in _elementsToDispose) - item.Dispose(); - } + public void Dispose() + { + foreach (IDisposable item in _elementsToDispose) + { + item.Dispose(); + } + } - private void OnButtonPressed(object? sender, ButtonPressedEventArgs e) + private void OnButtonPressed(object? sender, ButtonPressedEventArgs e) + { + if (Game1.activeClickableMenu is GameMenu gameMenu) + { + // Handle right trigger to switch to our mod options page + // NB The game does the correct thing for left trigger so we don't need to implement it. + if (e.Button == SButton.RightTrigger && !e.IsSuppressed()) { - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - // Handle right trigger to switch to our mod options page - // NB The game does the correct thing for left trigger so we don't need to implement it. - if (e.Button == SButton.RightTrigger && !e.IsSuppressed()) - { - if ((gameMenu.currentTab + 1 == _modOptionsTabPageNumber.Value) && gameMenu.readyToClose()) - { - ChangeToOurTab(gameMenu); - _helper.Input.Suppress(SButton.RightTrigger); - } - } - - // Based on GameMenu.receiveLeftClick and Game1.updateActiveMenu - if ((e.Button == SButton.MouseLeft || e.Button == SButton.ControllerA) && !e.IsSuppressed()) - { - // Workaround when exiting the map page because it calls GameMenu.changeTab and fails - if (gameMenu.currentTab == GameMenu.mapTab && gameMenu.lastOpenedNonMapTab == _modOptionsTabPageNumber.Value) - { - _changeToOurTabAfterTick.Value = true; - gameMenu.lastOpenedNonMapTab = GameMenu.optionsTab; - ModEntry.MonitorObject.Log($"{this.GetType().Name}: The map page is about to close and the menu will switch to our tab, applying workaround"); - } - - if (!gameMenu.invisible && !GameMenu.forcePreventClose) - { - const bool uiScale = true; - if (_modOptionsTab.Value?.containsPoint(Game1.getMouseX(uiScale), Game1.getMouseY(uiScale)) == true - && gameMenu.currentTab != _modOptionsTabPageNumber.Value - && gameMenu.readyToClose()) - { - ChangeToOurTab(gameMenu); - } - } - } - } + if (gameMenu.currentTab + 1 == _modOptionsTabPageNumber.Value && gameMenu.readyToClose()) + { + ChangeToOurTab(gameMenu); + _helper.Input.Suppress(SButton.RightTrigger); + } } - private void OnUpdateTicking(object? sender, EventArgs e) + // Based on GameMenu.receiveLeftClick and Game1.updateActiveMenu + if ((e.Button == SButton.MouseLeft || e.Button == SButton.ControllerA) && !e.IsSuppressed()) { - // Usually OnUpdateTicked catches the activeClickableMenu as soon as is it modified during the game update, - // so we always add our tab early enough. This is to handle the window resize workaround. - if (_addOurTabBeforeTick) + // Workaround when exiting the map page because it calls GameMenu.changeTab and fails + if (gameMenu.currentTab == GameMenu.mapTab && gameMenu.lastOpenedNonMapTab == _modOptionsTabPageNumber.Value) + { + _changeToOurTabAfterTick.Value = true; + gameMenu.lastOpenedNonMapTab = GameMenu.optionsTab; + ModEntry.MonitorObject.Log( + $"{GetType().Name}: The map page is about to close and the menu will switch to our tab, applying workaround" + ); + } + + if (!gameMenu.invisible && !GameMenu.forcePreventClose) + { + const bool uiScale = true; + if (_modOptionsTab.Value?.containsPoint(Game1.getMouseX(uiScale), Game1.getMouseY(uiScale)) == true && + gameMenu.currentTab != _modOptionsTabPageNumber.Value && + gameMenu.readyToClose()) { - _addOurTabBeforeTick = false; - GameRunner.instance.ExecuteForInstances((Game1 instance) => { - if (_lastMenu.Value != Game1.activeClickableMenu) - { - EarlyOnMenuChanged(_lastMenu.Value, Game1.activeClickableMenu); - _lastMenu.Value = Game1.activeClickableMenu; - } - - }); - ModEntry.MonitorObject.Log($"{this.GetType().Name}: Our tab was added back as the final step of the window resize workaround"); + ChangeToOurTab(gameMenu); } + } } + } + } - private void OnUpdateTicked(object? sender, EventArgs e) - { - var gameMenu = Game1.activeClickableMenu as GameMenu; - - // The map was closed and the last opened tab was ours - if (_changeToOurTabAfterTick.Value) - { - _changeToOurTabAfterTick.Value = false; - if (gameMenu != null) - { - ChangeToOurTab(gameMenu); - ModEntry.MonitorObject.Log($"{this.GetType().Name}: Changed back to our tab"); - } - } - + private void OnUpdateTicking(object? sender, EventArgs e) + { + // Usually OnUpdateTicked catches the activeClickableMenu as soon as is it modified during the game update, + // so we always add our tab early enough. This is to handle the window resize workaround. + if (_addOurTabBeforeTick) + { + _addOurTabBeforeTick = false; + GameRunner.instance.ExecuteForInstances( + instance => + { if (_lastMenu.Value != Game1.activeClickableMenu) { - EarlyOnMenuChanged(_lastMenu.Value, Game1.activeClickableMenu); - _lastMenu.Value = Game1.activeClickableMenu; - } - - if (_lastMenuTab.Value != gameMenu?.currentTab) - { - OnGameMenuTabChanged(gameMenu); - _lastMenuTab.Value = gameMenu?.currentTab; - } - } - - // Early because it is called during GameLoop.UpdateTicked instead of later during Display.MenuChanged, - private void EarlyOnMenuChanged(IClickableMenu? oldMenu, IClickableMenu? newMenu) - { - // Remove from old menu - if (oldMenu is GameMenu oldGameMenu) - { - if (_modOptionsPage.Value != null) - { - oldGameMenu.pages.Remove(_modOptionsPage.Value); - _modOptionsPage.Value = null; - } - if (_modOptionsPageButton.Value != null) - { - _modOptionsPageButton.Value = null; - } - _modOptionsTabPageNumber.Value = null; - _modOptionsTab.Value = null; + EarlyOnMenuChanged(_lastMenu.Value, Game1.activeClickableMenu); + _lastMenu.Value = Game1.activeClickableMenu; } + } + ); + ModEntry.MonitorObject.Log( + $"{GetType().Name}: Our tab was added back as the final step of the window resize workaround" + ); + } + } - // Add to new menu - if (newMenu is GameMenu newGameMenu) - { - // Both modOptions variables require Game1.activeClickableMenu to not be null. - if (_modOptionsPage.Value == null) - _modOptionsPage.Value = new ModOptionsPage(_optionsElements, _helper.Events); - if (_modOptionsPageButton.Value == null) - { - _modOptionsPageButton.Value = new ModOptionsPageButton(); - _modOptionsPageButton.Value.xPositionOnScreen = GetButtonXPosition(newGameMenu); - } - - List tabPages = newGameMenu.pages; - _modOptionsTabPageNumber.Value = tabPages.Count; - tabPages.Add(_modOptionsPage.Value); - - // Load last mod options page state - if (_savedPageState.Value != null) - { - _modOptionsPage.Value.LoadState(_savedPageState.Value); - _savedPageState.Value = null; - } - - // NB For menu tabs, name is the "id" of the tab and label is the hover text. - _modOptionsTab.Value = new ClickableComponent( - new Microsoft.Xna.Framework.Rectangle( - GetButtonXPosition(newGameMenu), - newGameMenu.yPositionOnScreen + IClickableMenu.tabYPositionRelativeToMenuY + 64, - 64, 64), - optionsTabName, - "ui2_mod_options") // Placeholder label that shouldn't be displayed - { - // The exit page tab ID is 12347 - myID = 12348, - leftNeighborID = 12347, - tryDefaultIfNoDownNeighborExists = true, - fullyImmutable = true - }; - - // Do not add our tab to GameMenu.tabs because GameMenu.draw will draw the menu tab incorrectly - // when our page is the current tab. - - var exitTab = newGameMenu.tabs.Find(tab => tab.myID == 12347); - if (exitTab != null) - { - exitTab.rightNeighborID = _modOptionsTab.Value.myID; - AddOurTabToClickableComponents(newGameMenu, _modOptionsTab.Value); - } - else - { - ModEntry.MonitorObject.LogOnce($"{this.GetType().Name}: Did not find the ExitPage tab in the new GameMenu.tabs", LogLevel.Error); - } - } - } + private void OnUpdateTicked(object? sender, EventArgs e) + { + var gameMenu = Game1.activeClickableMenu as GameMenu; - private void OnGameMenuTabChanged(GameMenu? gameMenu) + // The map was closed and the last opened tab was ours + if (_changeToOurTabAfterTick.Value) + { + _changeToOurTabAfterTick.Value = false; + if (gameMenu != null) { - if (gameMenu != null) - { - if (_modOptionsTab.Value != null) - { - // Update the downNeighborID for our tab - // Based on GameMenu.setTabNeighborsForCurrentPage - switch (gameMenu.currentTab) - { - case GameMenu.inventoryTab: - _modOptionsTab.Value.downNeighborID = downNeighborInInventory; - break; - case GameMenu.exitTab: - _modOptionsTab.Value.downNeighborID = 535; - break; - default: - _modOptionsTab.Value.downNeighborID = ClickableComponent.SNAP_TO_DEFAULT; - break; - } - - AddOurTabToClickableComponents(gameMenu, _modOptionsTab.Value); - } - } + ChangeToOurTab(gameMenu); + ModEntry.MonitorObject.Log($"{GetType().Name}: Changed back to our tab"); } + } + + if (_lastMenu.Value != Game1.activeClickableMenu) + { + EarlyOnMenuChanged(_lastMenu.Value, Game1.activeClickableMenu); + _lastMenu.Value = Game1.activeClickableMenu; + } + + if (_lastMenuTab.Value != gameMenu?.currentTab) + { + OnGameMenuTabChanged(gameMenu); + _lastMenuTab.Value = gameMenu?.currentTab; + } + } - private void OnRenderingMenu(object? sender, RenderingActiveMenuEventArgs e) + // Early because it is called during GameLoop.UpdateTicked instead of later during Display.MenuChanged, + private void EarlyOnMenuChanged(IClickableMenu? oldMenu, IClickableMenu? newMenu) + { + // Remove from old menu + if (oldMenu is GameMenu oldGameMenu) + { + if (_modOptionsPage.Value != null) { - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - // Draw our tab icon behind the menu even if it is dimmed by the menu's transparent background, - // so that it still displays during transitions eg. when a letter is viewed in the collections tab - DrawButton(gameMenu); - } + oldGameMenu.pages.Remove(_modOptionsPage.Value); + _modOptionsPage.Value = null; } - private void OnRenderedMenu(object? sender, RenderedActiveMenuEventArgs e) + if (_modOptionsPageButton.Value != null) { - if (Game1.activeClickableMenu is GameMenu gameMenu - // But don't render when the map is displayed... - && !(gameMenu.currentTab == GameMenu.mapTab - // ...or when a letter is opened in the collection's page - || gameMenu.GetCurrentPage() is CollectionsPage cPage && cPage.letterviewerSubMenu != null)) - { - DrawButton(gameMenu); - - Tools.DrawMouseCursor(); - - // Draw the game menu's hover text again so it displays above our tab - if (!gameMenu.hoverText.Equals("")) - IClickableMenu.drawHoverText(Game1.spriteBatch, gameMenu.hoverText, Game1.smallFont); + _modOptionsPageButton.Value = null; + } - // Draw our tab's hover text - if (_modOptionsTab.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) == true) - { - var tooltip = _helper.Translation.Get(LanguageKeys.OptionsTabTooltip).Default("UI Info Mod Options"); - IClickableMenu.drawHoverText(Game1.spriteBatch, tooltip, Game1.smallFont); + _modOptionsTabPageNumber.Value = null; + _modOptionsTab.Value = null; + } - if (!gameMenu.hoverText.Equals("")) - ModEntry.MonitorObject.LogOnce($"{(this.GetType().Name)}: Both our mod options tab and the game are displaying hover text", LogLevel.Warn); - } - } + // Add to new menu + if (newMenu is GameMenu newGameMenu) + { + // Both modOptions variables require Game1.activeClickableMenu to not be null. + if (_modOptionsPage.Value == null) + { + _modOptionsPage.Value = new ModOptionsPage(_optionsElements, _helper.Events); } - private void OnWindowClientSizeChanged(object? sender, EventArgs e) + if (_modOptionsPageButton.Value == null) { - _windowResizing = true; - GameRunner.instance.ExecuteForInstances((Game1 instance) => { - if (Game1.activeClickableMenu is GameMenu gameMenu - // NB SMAPI seems to use the game's instanceID as the screenID for PerScreen - && gameMenu.currentTab == _modOptionsTabPageNumber.GetValueForScreen(instance.instanceId)) - { - // Temporarily change all open mod options pages to the game's options page - // because the GameMenu is recreated when the window is resized, before we can add - // our mod options page to GameMenu#pages. - if (gameMenu.GetCurrentPage() is ModOptionsPage modOptionsPage) - { - _savedPageState.Value = new ModOptionsPageState(); - modOptionsPage.SaveState(_savedPageState.Value); - } - gameMenu.currentTab = GameMenu.optionsTab; - _instancesWithOptionsPageOpen.Add(instance.instanceId); - } - }); - if (_instancesWithOptionsPageOpen.Count > 0) - ModEntry.MonitorObject.Log($"{this.GetType().Name}: The window is being resized while our options page is opened, applying workaround"); + _modOptionsPageButton.Value = new ModOptionsPageButton(); + _modOptionsPageButton.Value.xPositionOnScreen = GetButtonXPosition(newGameMenu); } - // This seems to be called after Display.Rendered and Update.Ticking ie. between frames - private void OnWindowResized(object? sender, EventArgs e) + List tabPages = newGameMenu.pages; + _modOptionsTabPageNumber.Value = tabPages.Count; + tabPages.Add(_modOptionsPage.Value); + + // Load last mod options page state + if (_savedPageState.Value != null) { - if (_windowResizing) - { - _windowResizing = false; - if (_instancesWithOptionsPageOpen.Count > 0) - { - GameRunner.instance.ExecuteForInstances((Game1 instance) => { - if (_instancesWithOptionsPageOpen.Remove(instance.instanceId)) - { - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - gameMenu.currentTab = (int) _modOptionsTabPageNumber.GetValueForScreen(instance.instanceId)!; - } - } - }); - - ModEntry.MonitorObject.Log($"{this.GetType().Name}: The window was resized, reverting to our tab"); - _addOurTabBeforeTick = true; - } - } + _modOptionsPage.Value.LoadState(_savedPageState.Value); + _savedPageState.Value = null; } - /// Based on - private void ChangeToOurTab(GameMenu gameMenu) - { - var modOptionsTabIndex = (int) _modOptionsTabPageNumber.Value!; - gameMenu.currentTab = modOptionsTabIndex; - gameMenu.lastOpenedNonMapTab = modOptionsTabIndex; - gameMenu.initializeUpperRightCloseButton(); - gameMenu.invisible = false; - Game1.playSound("smallSelect"); - - // We don't need to call GameMenu.AddTabsToClickableComponents because populateClickableComponentList already does it for us. - // However, we must add our mod options tab now because snapToDefaultClickableComponent might use it. - gameMenu.GetCurrentPage().populateClickableComponentList(); - AddOurTabToClickableComponents(gameMenu, _modOptionsTab.Value!); - - gameMenu.setTabNeighborsForCurrentPage(); - if (Game1.options.SnappyMenus) - { - gameMenu.snapToDefaultClickableComponent(); - } + // NB For menu tabs, name is the "id" of the tab and label is the hover text. + _modOptionsTab.Value = new ClickableComponent( + new Rectangle( + GetButtonXPosition(newGameMenu), + newGameMenu.yPositionOnScreen + IClickableMenu.tabYPositionRelativeToMenuY + 64, + 64, + 64 + ), + optionsTabName, + "ui2_mod_options" + ) // Placeholder label that shouldn't be displayed + { + // The exit page tab ID is 12347 + myID = 12348, leftNeighborID = 12347, tryDefaultIfNoDownNeighborExists = true, fullyImmutable = true + }; + + // Do not add our tab to GameMenu.tabs because GameMenu.draw will draw the menu tab incorrectly + // when our page is the current tab. + + ClickableComponent? exitTab = newGameMenu.tabs.Find(tab => tab.myID == 12347); + if (exitTab != null) + { + exitTab.rightNeighborID = _modOptionsTab.Value.myID; + AddOurTabToClickableComponents(newGameMenu, _modOptionsTab.Value); + } + else + { + ModEntry.MonitorObject.LogOnce( + $"{GetType().Name}: Did not find the ExitPage tab in the new GameMenu.tabs", + LogLevel.Error + ); } + } + } - /// Add the tab to the current menu page's clickable components - /// It initializes the component list if needed, and doesn't add the tab if it is already present. - private void AddOurTabToClickableComponents(GameMenu gameMenu, ClickableComponent modOptionsTab) + private void OnGameMenuTabChanged(GameMenu? gameMenu) + { + if (gameMenu != null) + { + if (_modOptionsTab.Value != null) { - var currentPage = gameMenu.GetCurrentPage()!; - if (currentPage.allClickableComponents == null) - currentPage.populateClickableComponentList(); - - if (!currentPage.allClickableComponents!.Contains(modOptionsTab)) - currentPage.allClickableComponents.Add(modOptionsTab); + // Update the downNeighborID for our tab + // Based on GameMenu.setTabNeighborsForCurrentPage + if (gameMenu.currentTab == GameMenu.inventoryTab) + { + _modOptionsTab.Value.downNeighborID = downNeighborInInventory; + } + else if (gameMenu.currentTab == GameMenu.exitTab) + { + _modOptionsTab.Value.downNeighborID = 535; + } + else + { + _modOptionsTab.Value.downNeighborID = ClickableComponent.SNAP_TO_DEFAULT; + } + + AddOurTabToClickableComponents(gameMenu, _modOptionsTab.Value); } + } + } + + private void OnRenderingMenu(object? sender, RenderingActiveMenuEventArgs e) + { + if (Game1.activeClickableMenu is GameMenu gameMenu) + { + // Draw our tab icon behind the menu even if it is dimmed by the menu's transparent background, + // so that it still displays during transitions eg. when a letter is viewed in the collections tab + DrawButton(gameMenu); + } + } - private int GetButtonXPosition(GameMenu gameMenu) + private void OnRenderedMenu(object? sender, RenderedActiveMenuEventArgs e) + { + if (Game1.activeClickableMenu is GameMenu gameMenu + // But don't render when the map is displayed... + && + !(gameMenu.currentTab == GameMenu.mapTab + // ...or when a letter is opened in the collection's page + || + (gameMenu.GetCurrentPage() is CollectionsPage cPage && cPage.letterviewerSubMenu != null))) + { + DrawButton(gameMenu); + + Tools.DrawMouseCursor(); + + // Draw the game menu's hover text again so it displays above our tab + if (!gameMenu.hoverText.Equals("")) { - return gameMenu.xPositionOnScreen + gameMenu.width - 200; + IClickableMenu.drawHoverText(Game1.spriteBatch, gameMenu.hoverText, Game1.smallFont); } - private void DrawButton(GameMenu gameMenu) + // Draw our tab's hover text + if (_modOptionsTab.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) == true) { - var button = _modOptionsPageButton.Value!; - button.yPositionOnScreen = gameMenu.yPositionOnScreen + (gameMenu.currentTab == _modOptionsTabPageNumber.Value ? 24 : 16); - button.draw(Game1.spriteBatch); + Translation tooltip = _helper.Translation.Get(LanguageKeys.OptionsTabTooltip).Default("UI Info Mod Options"); + IClickableMenu.drawHoverText(Game1.spriteBatch, tooltip, Game1.smallFont); + + if (!gameMenu.hoverText.Equals("")) + { + ModEntry.MonitorObject.LogOnce( + $"{GetType().Name}: Both our mod options tab and the game are displaying hover text", + LogLevel.Warn + ); + } } + } + } - /// - /// Tries hard to return a version string for the mod like "v2.2.9" - /// - /// Try to get the version from the SMAPI manifest; then from the assembly in which case it is formatted as v=...; - /// then give up and return a default value. - /// - private static string GetVersionString(IModHelper helper) + private void OnWindowClientSizeChanged(object? sender, EventArgs e) + { + _windowResizing = true; + GameRunner.instance.ExecuteForInstances( + instance => { - var modInfo = helper.ModRegistry.Get(helper.ModRegistry.ModID); - if (modInfo != null) + if (Game1.activeClickableMenu is GameMenu gameMenu + // NB SMAPI seems to use the game's instanceID as the screenID for PerScreen + && + gameMenu.currentTab == _modOptionsTabPageNumber.GetValueForScreen(instance.instanceId)) + { + // Temporarily change all open mod options pages to the game's options page + // because the GameMenu is recreated when the window is resized, before we can add + // our mod options page to GameMenu#pages. + if (gameMenu.GetCurrentPage() is ModOptionsPage modOptionsPage) { - return $"v{modInfo.Manifest.Version}"; + _savedPageState.Value = new ModOptionsPageState(); + modOptionsPage.SaveState(_savedPageState.Value); } - ModEntry.MonitorObject.LogOnce($"{typeof(ModOptionsPageHandler).Name}: Couldn't retrieve our own mod information", LogLevel.Info); - var assemblyVersion = Assembly.GetAssembly(typeof(ModEntry))?.GetName().Version; - if (assemblyVersion != null) + gameMenu.currentTab = GameMenu.optionsTab; + _instancesWithOptionsPageOpen.Add(instance.instanceId); + } + } + ); + if (_instancesWithOptionsPageOpen.Count > 0) + { + ModEntry.MonitorObject.Log( + $"{GetType().Name}: The window is being resized while our options page is opened, applying workaround" + ); + } + } + + // This seems to be called after Display.Rendered and Update.Ticking ie. between frames + private void OnWindowResized(object? sender, EventArgs e) + { + if (_windowResizing) + { + _windowResizing = false; + if (_instancesWithOptionsPageOpen.Count > 0) + { + GameRunner.instance.ExecuteForInstances( + instance => { - return $"v={assemblyVersion}"; + if (_instancesWithOptionsPageOpen.Remove(instance.instanceId)) + { + if (Game1.activeClickableMenu is GameMenu gameMenu) + { + gameMenu.currentTab = (int)_modOptionsTabPageNumber.GetValueForScreen(instance.instanceId)!; + } + } } - ModEntry.MonitorObject.LogOnce($"{typeof(ModOptionsPageHandler).Name}: Couldn't retrieve our own assembly version information"); + ); - return $"(unknown version)"; + ModEntry.MonitorObject.Log($"{GetType().Name}: The window was resized, reverting to our tab"); + _addOurTabBeforeTick = true; } + } } - /// Data that is saved and restored when the the game menu is resized - internal class ModOptionsPageState + /// Based on + private void ChangeToOurTab(GameMenu gameMenu) + { + var modOptionsTabIndex = (int)_modOptionsTabPageNumber.Value!; + gameMenu.currentTab = modOptionsTabIndex; + gameMenu.lastOpenedNonMapTab = modOptionsTabIndex; + gameMenu.initializeUpperRightCloseButton(); + gameMenu.invisible = false; + Game1.playSound("smallSelect"); + + // We don't need to call GameMenu.AddTabsToClickableComponents because populateClickableComponentList already does it for us. + // However, we must add our mod options tab now because snapToDefaultClickableComponent might use it. + gameMenu.GetCurrentPage().populateClickableComponentList(); + AddOurTabToClickableComponents(gameMenu, _modOptionsTab.Value!); + + gameMenu.setTabNeighborsForCurrentPage(); + if (Game1.options.SnappyMenus) + { + gameMenu.snapToDefaultClickableComponent(); + } + } + + /// + /// Add the tab to the current menu page's clickable components + /// It initializes the component list if needed, and doesn't add the tab if it is already present. + /// + private void AddOurTabToClickableComponents(GameMenu gameMenu, ClickableComponent modOptionsTab) + { + IClickableMenu currentPage = gameMenu.GetCurrentPage()!; + if (currentPage.allClickableComponents == null) + { + currentPage.populateClickableComponentList(); + } + + if (!currentPage.allClickableComponents!.Contains(modOptionsTab)) + { + currentPage.allClickableComponents.Add(modOptionsTab); + } + } + + private int GetButtonXPosition(GameMenu gameMenu) + { + return gameMenu.xPositionOnScreen + gameMenu.width - 200; + } + + private void DrawButton(GameMenu gameMenu) + { + ModOptionsPageButton button = _modOptionsPageButton.Value!; + button.yPositionOnScreen = gameMenu.yPositionOnScreen + + (gameMenu.currentTab == _modOptionsTabPageNumber.Value ? 24 : 16); + button.draw(Game1.spriteBatch); + } + + /// + /// Tries hard to return a version string for the mod like "v2.2.9" + /// + /// Try to get the version from the SMAPI manifest; then from the assembly in which case it is formatted as v=...; + /// then give up and return a default value. + /// + /// + private static string GetVersionString(IModHelper helper) { - public int? currentIndex; - public int? currentComponent; + IModInfo? modInfo = helper.ModRegistry.Get(helper.ModRegistry.ModID); + if (modInfo != null) + { + return $"v{modInfo.Manifest.Version}"; + } + + ModEntry.MonitorObject.LogOnce( + $"{typeof(ModOptionsPageHandler).Name}: Couldn't retrieve our own mod information", + LogLevel.Info + ); + + Version? assemblyVersion = Assembly.GetAssembly(typeof(ModEntry))?.GetName().Version; + if (assemblyVersion != null) + { + return $"v={assemblyVersion}"; + } + + ModEntry.MonitorObject.LogOnce( + $"{typeof(ModOptionsPageHandler).Name}: Couldn't retrieve our own assembly version information" + ); + + return "(unknown version)"; } + } + + /// Data that is saved and restored when the the game menu is resized + internal class ModOptionsPageState + { + public int? currentComponent; + public int? currentIndex; + } } diff --git a/UIInfoSuite2/Options/ModOptionsPageIcon.cs b/UIInfoSuite2/Options/ModOptionsPageIcon.cs index c4c10138..709a6061 100644 --- a/UIInfoSuite2/Options/ModOptionsPageIcon.cs +++ b/UIInfoSuite2/Options/ModOptionsPageIcon.cs @@ -2,13 +2,8 @@ namespace UIInfoSuite2.Options { - internal class ModOptionsPageIcon : IClickableMenu - { - - - public override void receiveRightClick(int x, int y, bool playSound = true) - { - - } - } + internal class ModOptionsPageIcon : IClickableMenu + { + public override void receiveRightClick(int x, int y, bool playSound = true) { } + } } diff --git a/UIInfoSuite2/UIElements/ExperienceBar.cs b/UIInfoSuite2/UIElements/ExperienceBar.cs index ec5b8e0b..028b492f 100644 --- a/UIInfoSuite2/UIElements/ExperienceBar.cs +++ b/UIInfoSuite2/UIElements/ExperienceBar.cs @@ -1,3 +1,7 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using StardewModdingAPI; @@ -6,10 +10,6 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Tools; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; using UIInfoSuite2.Compatibility; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; @@ -17,371 +17,424 @@ namespace UIInfoSuite2.UIElements { - public partial class ExperienceBar : IDisposable + public partial class ExperienceBar : IDisposable + { + #region Properties + private readonly PerScreen _previousItem = new(); + private readonly PerScreen _currentExperience = new(() => new int[5]); + private readonly PerScreen _currentLevelExtenderExperience = new(() => new int[5]); + private readonly PerScreen _currentSkillLevel = new(() => 0); + private readonly PerScreen _experienceRequiredToLevel = new(() => -1); + private readonly PerScreen _experienceFromPreviousLevels = new(() => -1); + private readonly PerScreen _experienceEarnedThisLevel = new(() => -1); + + private readonly PerScreen _displayedExperienceBar = + new(() => new DisplayedExperienceBar()); + + private readonly PerScreen _displayedLevelUpMessage = + new(() => new DisplayedLevelUpMessage()); + + private readonly PerScreen> _displayedExperienceValues = + new(() => new List()); + + private const int LevelUpVisibleTicks = 120; + private readonly PerScreen _levelUpVisibleTimer = new(); + private const int ExperienceBarVisibleTicks = 480; + private readonly PerScreen _experienceBarVisibleTimer = new(); + + private static readonly Dictionary SkillIconRectangles = new() { - #region Properties - - private readonly PerScreen _previousItem = new(); - private readonly PerScreen _currentExperience = new(createNewState: () => new int[5]); - private readonly PerScreen _currentLevelExtenderExperience = new(createNewState: () => new int[5]); - private readonly PerScreen _currentSkillLevel = new(createNewState: () => 0); - private readonly PerScreen _experienceRequiredToLevel = new(createNewState: () => -1); - private readonly PerScreen _experienceFromPreviousLevels = new(createNewState: () => -1); - private readonly PerScreen _experienceEarnedThisLevel = new(createNewState: () => -1); - - private readonly PerScreen _displayedExperienceBar = new(createNewState: () => new DisplayedExperienceBar()); - private readonly PerScreen _displayedLevelUpMessage = new(createNewState: () => new DisplayedLevelUpMessage()); - private readonly PerScreen> _displayedExperienceValues = new(createNewState: () => new List()); - - private const int LevelUpVisibleTicks = 120; - private readonly PerScreen _levelUpVisibleTimer = new(); - private const int ExperienceBarVisibleTicks = 480; - private readonly PerScreen _experienceBarVisibleTimer = new(); - - private static readonly Dictionary SkillIconRectangles = new() - { - { SkillType.Farming , new Rectangle(10, 428, 10, 10)}, - { SkillType.Fishing , new Rectangle(20, 428, 10, 10)}, - { SkillType.Foraging , new Rectangle(60, 428, 10, 10)}, - { SkillType.Mining , new Rectangle(30, 428, 10, 10)}, - { SkillType.Combat , new Rectangle(120, 428, 10, 10)} - }; - - private static readonly Dictionary ExperienceFillColor = new() - { - { SkillType.Farming , new Color(255, 251, 35, 0.38f)}, - { SkillType.Fishing , new Color(17, 84, 252, 0.63f)}, - { SkillType.Foraging , new Color(0, 234, 0, 0.63f)}, - { SkillType.Mining , new Color(145, 104, 63, 0.63f)}, - { SkillType.Combat , new Color(204, 0, 3, 0.63f)} - }; - - private readonly PerScreen _experienceIconRectangle = new(createNewState: () => SkillIconRectangles[SkillType.Farming]); - private readonly PerScreen _levelUpIconRectangle = new(createNewState: () => SkillIconRectangles[SkillType.Farming]); - private readonly PerScreen _experienceFillColor = new(createNewState: () => ExperienceFillColor[SkillType.Farming]); + { SkillType.Farming, new Rectangle(10, 428, 10, 10) }, + { SkillType.Fishing, new Rectangle(20, 428, 10, 10) }, + { SkillType.Foraging, new Rectangle(60, 428, 10, 10) }, + { SkillType.Mining, new Rectangle(30, 428, 10, 10) }, + { SkillType.Combat, new Rectangle(120, 428, 10, 10) } + }; + + private static readonly Dictionary ExperienceFillColor = new() + { + { SkillType.Farming, new Color(255, 251, 35, 0.38f) }, + { SkillType.Fishing, new Color(17, 84, 252, 0.63f) }, + { SkillType.Foraging, new Color(0, 234, 0, 0.63f) }, + { SkillType.Mining, new Color(145, 104, 63, 0.63f) }, + { SkillType.Combat, new Color(204, 0, 3, 0.63f) } + }; - private SoundEffectInstance _soundEffect; + private readonly PerScreen _experienceIconRectangle = new(() => SkillIconRectangles[SkillType.Farming]); - private bool ExperienceBarFadeoutEnabled { get; set; } = true; - private bool ExperienceGainTextEnabled { get; set; } = true; - private bool LevelUpAnimationEnabled { get; set; } = true; - private bool ExperienceBarEnabled { get; set; } = true; + private readonly PerScreen _levelUpIconRectangle = new(() => SkillIconRectangles[SkillType.Farming]); + private readonly PerScreen _experienceFillColor = new(() => ExperienceFillColor[SkillType.Farming]); - private readonly IModHelper _helper; - private readonly ILevelExtender _levelExtenderApi; + private SoundEffectInstance _soundEffect; - #endregion Properties + private bool ExperienceBarFadeoutEnabled { get; set; } = true; + private bool ExperienceGainTextEnabled { get; set; } = true; + private bool LevelUpAnimationEnabled { get; set; } = true; + private bool ExperienceBarEnabled { get; set; } = true; - #region Lifecycle + private readonly IModHelper _helper; + private readonly ILevelExtender _levelExtenderApi; + #endregion Properties - public ExperienceBar(IModHelper helper) - { - _helper = helper; - - InitializeSound(); + #region Lifecycle + public ExperienceBar(IModHelper helper) + { + _helper = helper; - if (_helper.ModRegistry.IsLoaded("DevinLematty.LevelExtender")) - { - _levelExtenderApi = _helper.ModRegistry.GetApi("DevinLematty.LevelExtender"); - } - } + InitializeSound(); - private void InitializeSound() - { - string path = string.Empty; - try - { - path = Path.Combine(_helper.DirectoryPath, "Assets", "LevelUp.wav"); - _soundEffect = SoundEffect.FromStream(new FileStream(path, FileMode.Open)).CreateInstance(); - } - catch (Exception ex) - { - ModEntry.MonitorObject.Log( - "Error loading sound file from " + path + ": " + ex.Message + Environment.NewLine + ex.StackTrace, - LogLevel.Error); - } - } + if (_helper.ModRegistry.IsLoaded("DevinLematty.LevelExtender")) + { + _levelExtenderApi = _helper.ModRegistry.GetApi("DevinLematty.LevelExtender"); + } + } - public void Dispose() - { + private void InitializeSound() + { + var path = string.Empty; + try + { + path = Path.Combine(_helper.DirectoryPath, "Assets", "LevelUp.wav"); + _soundEffect = SoundEffect.FromStream(new FileStream(path, FileMode.Open)).CreateInstance(); + } + catch (Exception ex) + { + ModEntry.MonitorObject.Log( + "Error loading sound file from " + path + ": " + ex.Message + Environment.NewLine + ex.StackTrace, + LogLevel.Error + ); + } + } + public void Dispose() + { + _soundEffect.Dispose(); + } - _soundEffect.Dispose(); - } + public void ToggleOption( + bool experienceBarEnabled, + bool experienceBarFadeoutEnabled, + bool experienceGainTextEnabled, + bool levelUpAnimationEnabled + ) + { + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Player.Warped -= OnWarped; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked_HandleTimers; + _helper.Events.GameLoop.SaveLoaded -= OnSaveLoaded; + + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked_UpdateExperience; + _helper.Events.Player.LevelChanged -= OnLevelChanged; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked_UpdateExperience; + + ExperienceBarEnabled = experienceBarEnabled; + ExperienceBarFadeoutEnabled = experienceBarFadeoutEnabled; + ExperienceGainTextEnabled = experienceGainTextEnabled; + LevelUpAnimationEnabled = levelUpAnimationEnabled; + + if (ExperienceBarEnabled || ExperienceBarFadeoutEnabled || ExperienceGainTextEnabled || LevelUpAnimationEnabled) + { + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Player.Warped += OnWarped; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked_HandleTimers; + _helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; + } + + if (ExperienceBarEnabled || ExperienceGainTextEnabled) + { + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked_UpdateExperience; + } + + if (LevelUpAnimationEnabled) + { + _helper.Events.Player.LevelChanged += OnLevelChanged; + } + } - public void ToggleOption(bool experienceBarEnabled, bool experienceBarFadeoutEnabled, bool experienceGainTextEnabled, bool levelUpAnimationEnabled) - { - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Player.Warped -= OnWarped; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked_HandleTimers; - _helper.Events.GameLoop.SaveLoaded -= OnSaveLoaded; - - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked_UpdateExperience; - _helper.Events.Player.LevelChanged -= OnLevelChanged; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked_UpdateExperience; - - ExperienceBarEnabled = experienceBarEnabled; - ExperienceBarFadeoutEnabled = experienceBarFadeoutEnabled; - ExperienceGainTextEnabled = experienceGainTextEnabled; - LevelUpAnimationEnabled = levelUpAnimationEnabled; - - if (ExperienceBarEnabled || ExperienceBarFadeoutEnabled || ExperienceGainTextEnabled || LevelUpAnimationEnabled) - { - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Player.Warped += OnWarped; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked_HandleTimers; - _helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; - } - - if (ExperienceBarEnabled || ExperienceGainTextEnabled) - { - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked_UpdateExperience; - } - - if (LevelUpAnimationEnabled) - { - _helper.Events.Player.LevelChanged += OnLevelChanged; - } - } + public void ToggleShowExperienceBar(bool experienceBarEnabled) + { + ToggleOption( + experienceBarEnabled, + ExperienceBarFadeoutEnabled, + ExperienceGainTextEnabled, + LevelUpAnimationEnabled + ); + } - public void ToggleShowExperienceBar(bool experienceBarEnabled) - { - ToggleOption(experienceBarEnabled, ExperienceBarFadeoutEnabled, ExperienceGainTextEnabled, LevelUpAnimationEnabled); - } + public void ToggleExperienceBarFade(bool experienceBarFadeoutEnabled) + { + ToggleOption( + ExperienceBarEnabled, + experienceBarFadeoutEnabled, + ExperienceGainTextEnabled, + LevelUpAnimationEnabled + ); + } - public void ToggleExperienceBarFade(bool experienceBarFadeoutEnabled) - { - ToggleOption(ExperienceBarEnabled, experienceBarFadeoutEnabled, ExperienceGainTextEnabled, LevelUpAnimationEnabled); - } + public void ToggleShowExperienceGain(bool experienceGainTextEnabled) + { + InitializeExperiencePoints(); + ToggleOption( + ExperienceBarEnabled, + ExperienceBarFadeoutEnabled, + experienceGainTextEnabled, + LevelUpAnimationEnabled + ); + } - public void ToggleShowExperienceGain(bool experienceGainTextEnabled) - { - InitializeExperiencePoints(); - ToggleOption(ExperienceBarEnabled, ExperienceBarFadeoutEnabled, experienceGainTextEnabled, LevelUpAnimationEnabled); - } + public void ToggleLevelUpAnimation(bool levelUpAnimationEnabled) + { + ToggleOption( + ExperienceBarEnabled, + ExperienceBarFadeoutEnabled, + ExperienceGainTextEnabled, + levelUpAnimationEnabled + ); + } + #endregion Lifecycle - public void ToggleLevelUpAnimation(bool levelUpAnimationEnabled) - { - ToggleOption(ExperienceBarEnabled, ExperienceBarFadeoutEnabled, ExperienceGainTextEnabled, levelUpAnimationEnabled); - } + #region Event subscriptions + private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) + { + InitializeExperiencePoints(); - #endregion Lifecycle + _displayedExperienceValues.Value.Clear(); + } - #region Event subscriptions + private void OnLevelChanged(object sender, LevelChangedEventArgs e) + { + if (LevelUpAnimationEnabled && e.IsLocalPlayer) + { + _levelUpVisibleTimer.Value = LevelUpVisibleTicks; + _levelUpIconRectangle.Value = SkillIconRectangles[e.Skill]; - private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) - { - InitializeExperiencePoints(); + _experienceBarVisibleTimer.Value = ExperienceBarVisibleTicks; - _displayedExperienceValues.Value.Clear(); - } + PlayLevelUpSoundEffect(); + } + } - private void OnLevelChanged(object sender, LevelChangedEventArgs e) - { - if (LevelUpAnimationEnabled && e.IsLocalPlayer) - { - _levelUpVisibleTimer.Value = LevelUpVisibleTicks; - _levelUpIconRectangle.Value = SkillIconRectangles[e.Skill]; + private void OnWarped(object sender, WarpedEventArgs e) + { + if (e.IsLocalPlayer) + { + _displayedExperienceValues.Value.Clear(); + } + } - _experienceBarVisibleTimer.Value = ExperienceBarVisibleTicks; + private void OnUpdateTicked_UpdateExperience(object sender, UpdateTickedEventArgs e) + { + if (!e.IsMultipleOf(15)) // quarter second + { + return; + } + + bool skillChanged = TryGetCurrentLevelIndexFromSkillChange(out int currentLevelIndex); + bool itemChanged = Game1.player.CurrentItem != _previousItem.Value; + + if (itemChanged) + { + currentLevelIndex = GetCurrentLevelIndexFromItemChange(Game1.player.CurrentItem); + _previousItem.Value = Game1.player.CurrentItem; + } + + if (skillChanged || itemChanged) + { + UpdateExperience(currentLevelIndex, skillChanged); + } + } - PlayLevelUpSoundEffect(); - } - } + public void OnUpdateTicked_HandleTimers(object sender, UpdateTickedEventArgs e) + { + if (_levelUpVisibleTimer.Value > 0) + { + _levelUpVisibleTimer.Value--; + } + + if (_experienceBarVisibleTimer.Value > 0) + { + _experienceBarVisibleTimer.Value--; + } + } - private void OnWarped(object sender, WarpedEventArgs e) + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + if (Game1.eventUp) + { + return; + } + + // Level up text + if (LevelUpAnimationEnabled && _levelUpVisibleTimer.Value != 0) + { + _displayedLevelUpMessage.Value.Draw(_levelUpIconRectangle.Value, _helper.SafeGetString(LanguageKeys.LevelUp)); + } + + // Experience values + for (int i = _displayedExperienceValues.Value.Count - 1; i >= 0; --i) + { + if (_displayedExperienceValues.Value[i].IsInvisible) { - if (e.IsLocalPlayer) - _displayedExperienceValues.Value.Clear(); + _displayedExperienceValues.Value.RemoveAt(i); } - - private void OnUpdateTicked_UpdateExperience(object sender, UpdateTickedEventArgs e) + else { - if (!e.IsMultipleOf(15)) // quarter second - return; - - bool skillChanged = TryGetCurrentLevelIndexFromSkillChange(out int currentLevelIndex); - bool itemChanged = Game1.player.CurrentItem != _previousItem.Value; - - if (itemChanged) - { - currentLevelIndex = GetCurrentLevelIndexFromItemChange(Game1.player.CurrentItem); - _previousItem.Value = Game1.player.CurrentItem; - } - - if (skillChanged || itemChanged) - { - UpdateExperience(currentLevelIndex, skillChanged); - } + if (ExperienceGainTextEnabled) + { + _displayedExperienceValues.Value[i].Draw(); + } } + } + + // Experience bar + if (ExperienceBarEnabled && + (_experienceBarVisibleTimer.Value != 0 || !ExperienceBarFadeoutEnabled) && + _experienceRequiredToLevel.Value > 0) + { + _displayedExperienceBar.Value.Draw( + _experienceFillColor.Value, + _experienceIconRectangle.Value, + _experienceEarnedThisLevel.Value, + _experienceRequiredToLevel.Value - _experienceFromPreviousLevels.Value, + _currentSkillLevel.Value + ); + } + } + #endregion Event subscriptions - public void OnUpdateTicked_HandleTimers(object sender, UpdateTickedEventArgs e) + #region Logic + private void InitializeExperiencePoints() + { + for (var i = 0; i < _currentExperience.Value.Length; ++i) + { + _currentExperience.Value[i] = Game1.player.experiencePoints[i]; + } + + if (_levelExtenderApi != null) + { + for (var i = 0; i < _currentLevelExtenderExperience.Value.Length; ++i) { - if (_levelUpVisibleTimer.Value > 0) - { - _levelUpVisibleTimer.Value--; - } - - if (_experienceBarVisibleTimer.Value > 0) - { - _experienceBarVisibleTimer.Value--; - } + _currentLevelExtenderExperience.Value[i] = _levelExtenderApi.CurrentXP()[i]; } + } + } - private void OnRenderingHud(object sender, RenderingHudEventArgs e) + private void PlayLevelUpSoundEffect() + { + if (_soundEffect == null) + { + return; + } + + _soundEffect.Volume = Game1.options.soundVolumeLevel; + Task.Factory.StartNew( + async () => { - if (Game1.eventUp) - return; - - // Level up text - if (LevelUpAnimationEnabled && _levelUpVisibleTimer.Value != 0) - { - _displayedLevelUpMessage.Value.Draw(_levelUpIconRectangle.Value, _helper.SafeGetString(LanguageKeys.LevelUp)); - } - - // Experience values - for (int i = _displayedExperienceValues.Value.Count - 1; i >= 0; --i) - { - if (_displayedExperienceValues.Value[i].IsInvisible) - { - _displayedExperienceValues.Value.RemoveAt(i); - } - else - { - if (ExperienceGainTextEnabled) - _displayedExperienceValues.Value[i].Draw(); - } - } - - // Experience bar - if (ExperienceBarEnabled && (_experienceBarVisibleTimer.Value != 0 || !ExperienceBarFadeoutEnabled) && _experienceRequiredToLevel.Value > 0) - { - _displayedExperienceBar.Value.Draw(_experienceFillColor.Value, _experienceIconRectangle.Value, - _experienceEarnedThisLevel.Value, _experienceRequiredToLevel.Value - _experienceFromPreviousLevels.Value, _currentSkillLevel.Value); - } + await Task.Delay(200); + _soundEffect?.Play(); } + ); + } - #endregion Event subscriptions - - #region Logic + private bool TryGetCurrentLevelIndexFromSkillChange(out int currentLevelIndex) + { + currentLevelIndex = -1; - private void InitializeExperiencePoints() + for (var i = 0; i < _currentExperience.Value.Length; ++i) + { + if (_currentExperience.Value[i] != Game1.player.experiencePoints[i] || + (_levelExtenderApi != null && _currentLevelExtenderExperience.Value[i] != _levelExtenderApi.CurrentXP()[i])) { - for (var i = 0; i < _currentExperience.Value.Length; ++i) - _currentExperience.Value[i] = Game1.player.experiencePoints[i]; - if (_levelExtenderApi != null) - { - for (var i = 0; i < _currentLevelExtenderExperience.Value.Length; ++i) - _currentLevelExtenderExperience.Value[i] = _levelExtenderApi.CurrentXP()[i]; - } + currentLevelIndex = i; + break; } + } - private void PlayLevelUpSoundEffect() - { - if (_soundEffect == null) - return; - - _soundEffect.Volume = Game1.options.soundVolumeLevel; - Task.Factory.StartNew(async () => - { - await Task.Delay(200); - _soundEffect?.Play(); - }); - } + return currentLevelIndex != -1; + } - private bool TryGetCurrentLevelIndexFromSkillChange(out int currentLevelIndex) - { - currentLevelIndex = -1; - - for (var i = 0; i < _currentExperience.Value.Length; ++i) - { - if (_currentExperience.Value[i] != Game1.player.experiencePoints[i] || - (_levelExtenderApi != null && _currentLevelExtenderExperience.Value[i] != _levelExtenderApi.CurrentXP()[i])) - { - currentLevelIndex = i; - break; - } - } - - return currentLevelIndex != -1; - } + private static int GetCurrentLevelIndexFromItemChange(Item currentItem) + { + return currentItem switch + { + FishingRod => (int)SkillType.Fishing, + Pickaxe => (int)SkillType.Mining, + MeleeWeapon weapon when weapon.Name != "Scythe" => (int)SkillType.Combat, + _ when Game1.currentLocation is Farm && currentItem is not Axe => (int)SkillType.Farming, + _ => (int)SkillType.Foraging + }; + } - private static int GetCurrentLevelIndexFromItemChange(Item currentItem) + private void UpdateExperience(int currentLevelIndex, bool displayExperience) + { + _experienceBarVisibleTimer.Value = ExperienceBarVisibleTicks; + + _experienceIconRectangle.Value = SkillIconRectangles[(SkillType)currentLevelIndex]; + _experienceFillColor.Value = ExperienceFillColor[(SkillType)currentLevelIndex]; + _currentSkillLevel.Value = Game1.player.GetSkillLevel(currentLevelIndex); + + _experienceRequiredToLevel.Value = GetExperienceRequiredToLevel(_currentSkillLevel.Value); + _experienceFromPreviousLevels.Value = GetExperienceRequiredToLevel(_currentSkillLevel.Value - 1); + _experienceEarnedThisLevel.Value = + Game1.player.experiencePoints[currentLevelIndex] - _experienceFromPreviousLevels.Value; + + if (_experienceRequiredToLevel.Value <= 0 && _levelExtenderApi != null) + { + _experienceEarnedThisLevel.Value = _levelExtenderApi.CurrentXP()[currentLevelIndex]; + _experienceFromPreviousLevels.Value = + _currentExperience.Value[currentLevelIndex] - _experienceEarnedThisLevel.Value; + _experienceRequiredToLevel.Value = _levelExtenderApi.RequiredXP()[currentLevelIndex] + + _experienceFromPreviousLevels.Value; + } + + if (displayExperience) + { + if (ExperienceGainTextEnabled && _experienceRequiredToLevel.Value > 0) { - return currentItem switch - { - FishingRod => (int)SkillType.Fishing, - Pickaxe => (int)SkillType.Mining, - MeleeWeapon weapon when weapon.Name != "Scythe" => (int)SkillType.Combat, - _ when Game1.currentLocation is Farm && currentItem is not Axe => (int)SkillType.Farming, - _ => (int)SkillType.Foraging - }; + int currentExperienceToUse = Game1.player.experiencePoints[currentLevelIndex]; + int previousExperienceToUse = _currentExperience.Value[currentLevelIndex]; + if (_levelExtenderApi != null && _currentSkillLevel.Value > 9) + { + currentExperienceToUse = _levelExtenderApi.CurrentXP()[currentLevelIndex]; + previousExperienceToUse = _currentLevelExtenderExperience.Value[currentLevelIndex]; + } + + int experienceGain = currentExperienceToUse - previousExperienceToUse; + + if (experienceGain > 0) + { + _displayedExperienceValues.Value.Add( + new DisplayedExperienceValue(experienceGain, Game1.player.getLocalPosition(Game1.viewport)) + ); + } } - private void UpdateExperience(int currentLevelIndex, bool displayExperience) + _currentExperience.Value[currentLevelIndex] = Game1.player.experiencePoints[currentLevelIndex]; + + if (_levelExtenderApi != null) { - _experienceBarVisibleTimer.Value = ExperienceBarVisibleTicks; - - _experienceIconRectangle.Value = SkillIconRectangles[(SkillType)currentLevelIndex]; - _experienceFillColor.Value = ExperienceFillColor[(SkillType)currentLevelIndex]; - _currentSkillLevel.Value = Game1.player.GetSkillLevel(currentLevelIndex); - - _experienceRequiredToLevel.Value = GetExperienceRequiredToLevel(_currentSkillLevel.Value); - _experienceFromPreviousLevels.Value = GetExperienceRequiredToLevel(_currentSkillLevel.Value - 1); - _experienceEarnedThisLevel.Value = Game1.player.experiencePoints[currentLevelIndex] - _experienceFromPreviousLevels.Value; - - if (_experienceRequiredToLevel.Value <= 0 && _levelExtenderApi != null) - { - _experienceEarnedThisLevel.Value = _levelExtenderApi.CurrentXP()[currentLevelIndex]; - _experienceFromPreviousLevels.Value = _currentExperience.Value[currentLevelIndex] - _experienceEarnedThisLevel.Value; - _experienceRequiredToLevel.Value = _levelExtenderApi.RequiredXP()[currentLevelIndex] + _experienceFromPreviousLevels.Value; - } - - if (displayExperience) - { - if (ExperienceGainTextEnabled && _experienceRequiredToLevel.Value > 0) - { - int currentExperienceToUse = Game1.player.experiencePoints[currentLevelIndex]; - var previousExperienceToUse = _currentExperience.Value[currentLevelIndex]; - if (_levelExtenderApi != null && _currentSkillLevel.Value > 9) - { - currentExperienceToUse = _levelExtenderApi.CurrentXP()[currentLevelIndex]; - previousExperienceToUse = _currentLevelExtenderExperience.Value[currentLevelIndex]; - } - - int experienceGain = currentExperienceToUse - previousExperienceToUse; - - if (experienceGain > 0) - { - _displayedExperienceValues.Value.Add( - new DisplayedExperienceValue( - experienceGain, - Game1.player.getLocalPosition(Game1.viewport))); - } - } - - _currentExperience.Value[currentLevelIndex] = Game1.player.experiencePoints[currentLevelIndex]; - - if (_levelExtenderApi != null) - _currentLevelExtenderExperience.Value[currentLevelIndex] = _levelExtenderApi.CurrentXP()[currentLevelIndex]; - } + _currentLevelExtenderExperience.Value[currentLevelIndex] = _levelExtenderApi.CurrentXP()[currentLevelIndex]; } + } + } - private static int GetExperienceRequiredToLevel(int currentLevel) => currentLevel switch - { - 0 => 100, - 1 => 380, - 2 => 770, - 3 => 1300, - 4 => 2150, - 5 => 3300, - 6 => 4800, - 7 => 6900, - 8 => 10000, - 9 => 15000, - _ => -1 - }; - - #endregion Logic + private static int GetExperienceRequiredToLevel(int currentLevel) + { + return currentLevel switch + { + 0 => 100, + 1 => 380, + 2 => 770, + 3 => 1300, + 4 => 2150, + 5 => 3300, + 6 => 4800, + 7 => 6900, + 8 => 10000, + 9 => 15000, + _ => -1 + }; } -} \ No newline at end of file + #endregion Logic + } +} diff --git a/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceBar.cs b/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceBar.cs index d7b02992..c3bbe8c4 100644 --- a/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceBar.cs +++ b/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceBar.cs @@ -1,133 +1,147 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewValley; using StardewValley.Menus; -using System; namespace UIInfoSuite2.UIElements.ExperienceElements { - public class DisplayedExperienceBar + public class DisplayedExperienceBar + { + private const int MaxBarWidth = 175; + + public void Draw( + Color experienceFillColor, + Rectangle experienceIconPosition, + int experienceEarnedThisLevel, + int experienceDifferenceBetweenLevels, + int currentLevel + ) { - private const int MaxBarWidth = 175; - - public void Draw(Color experienceFillColor, Rectangle experienceIconPosition, - int experienceEarnedThisLevel, int experienceDifferenceBetweenLevels, int currentLevel) - { - int barWidth = GetBarWidth(experienceEarnedThisLevel, experienceDifferenceBetweenLevels); - float leftSide = GetExperienceBarLeftSide(); - - Game1.drawDialogueBox( - (int)leftSide, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 160, - 240, - 160, - false, - true); - - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, - barWidth, - 31), experienceFillColor); - - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, - Math.Min(4, barWidth), - 31), experienceFillColor); - - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, - barWidth, - 4), experienceFillColor); - - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - (int)leftSide + 32, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 36, - barWidth, - 4), experienceFillColor); - - if (IsMouseOverExperienceBar(leftSide)) - { - Game1.drawWithBorder( - experienceEarnedThisLevel + "/" + experienceDifferenceBetweenLevels, - Color.Black, - Color.Black, - new Vector2( - leftSide + 33, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70)); - } - else - { - Game1.spriteBatch.Draw( - Game1.mouseCursors, - new Vector2( - leftSide + 54, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 62), experienceIconPosition, Color.White, - 0, - Vector2.Zero, - 2.9f, - SpriteEffects.None, - 0.85f); - - Game1.drawWithBorder( - currentLevel.ToString(), - Color.Black * 0.6f, - Color.Black, - new Vector2( - leftSide + 33, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70)); - } - } - - #region Static helpers - - private static int GetBarWidth(int experienceEarnedThisLevel, int experienceDifferenceBetweenLevels) => - (int)((double)experienceEarnedThisLevel / experienceDifferenceBetweenLevels * MaxBarWidth); + int barWidth = GetBarWidth(experienceEarnedThisLevel, experienceDifferenceBetweenLevels); + float leftSide = GetExperienceBarLeftSide(); + + Game1.drawDialogueBox( + (int)leftSide, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 160, + 240, + 160, + false, + true + ); + + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, + barWidth, + 31 + ), + experienceFillColor + ); + + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, + Math.Min(4, barWidth), + 31 + ), + experienceFillColor + ); + + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 63, + barWidth, + 4 + ), + experienceFillColor + ); + + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + (int)leftSide + 32, + Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 36, + barWidth, + 4 + ), + experienceFillColor + ); + + if (IsMouseOverExperienceBar(leftSide)) + { + Game1.drawWithBorder( + experienceEarnedThisLevel + "/" + experienceDifferenceBetweenLevels, + Color.Black, + Color.Black, + new Vector2(leftSide + 33, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70) + ); + } + else + { + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(leftSide + 54, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 62), + experienceIconPosition, + Color.White, + 0, + Vector2.Zero, + 2.9f, + SpriteEffects.None, + 0.85f + ); + + Game1.drawWithBorder( + currentLevel.ToString(), + Color.Black * 0.6f, + Color.Black, + new Vector2(leftSide + 33, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 70) + ); + } + } - private static float GetExperienceBarLeftSide() - { - float leftSide = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Left; + #region Static helpers + private static int GetBarWidth(int experienceEarnedThisLevel, int experienceDifferenceBetweenLevels) + { + return (int)((double)experienceEarnedThisLevel / experienceDifferenceBetweenLevels * MaxBarWidth); + } - if (Game1.isOutdoorMapSmallerThanViewport()) - { - int num3 = Game1.currentLocation.map.Layers[0].LayerWidth * Game1.tileSize; - leftSide += (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right - num3) / 2; - } + private static float GetExperienceBarLeftSide() + { + float leftSide = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Left; - return leftSide; - } + if (Game1.isOutdoorMapSmallerThanViewport()) + { + int num3 = Game1.currentLocation.map.Layers[0].LayerWidth * Game1.tileSize; + leftSide += (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Right - num3) / 2; + } - private static bool IsMouseOverExperienceBar(float leftSide) - { - return GetExperienceBarTextureComponent(leftSide).containsPoint(Game1.getMouseX(), Game1.getMouseY()); - } + return leftSide; + } - private static ClickableTextureComponent GetExperienceBarTextureComponent(float leftSide) - { - return new ClickableTextureComponent( - "", - new Rectangle( - (int)leftSide - 36, - Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 80, - 260, - 100), - "", - "", - Game1.mouseCursors, - new Rectangle(0, 0, 0, 0), - Game1.pixelZoom); - } + private static bool IsMouseOverExperienceBar(float leftSide) + { + return GetExperienceBarTextureComponent(leftSide).containsPoint(Game1.getMouseX(), Game1.getMouseY()); + } - #endregion + private static ClickableTextureComponent GetExperienceBarTextureComponent(float leftSide) + { + return new ClickableTextureComponent( + "", + new Rectangle((int)leftSide - 36, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - 80, 260, 100), + "", + "", + Game1.mouseCursors, + new Rectangle(0, 0, 0, 0), + Game1.pixelZoom + ); } -} \ No newline at end of file + #endregion + } +} diff --git a/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceValue.cs b/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceValue.cs index 6868e30c..8a7ee829 100644 --- a/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceValue.cs +++ b/UIInfoSuite2/UIElements/ExperienceElements/DisplayedExperienceValue.cs @@ -3,34 +3,35 @@ namespace UIInfoSuite2.UIElements.ExperienceElements { - internal class DisplayedExperienceValue - { - private readonly float _experiencePoints; - private Vector2 _position; + internal class DisplayedExperienceValue + { + private readonly float _experiencePoints; - private int _alpha = 100; + private int _alpha = 100; + private Vector2 _position; - public DisplayedExperienceValue(float experiencePoints, Vector2 position) - { - _experiencePoints = experiencePoints; - _position = position; - } + public DisplayedExperienceValue(float experiencePoints, Vector2 position) + { + _experiencePoints = experiencePoints; + _position = position; + } - public void Draw() - { - _position.Y -= 0.5f; - --_alpha; + public bool IsInvisible => _alpha < 3; - Game1.drawWithBorder( - "Exp " + _experiencePoints, - Color.DarkSlateGray * (_alpha / 100f), - Color.PaleTurquoise * (_alpha / 100f), - Utility.ModifyCoordinatesForUIScale(new Vector2(_position.X - 28, _position.Y - 130)), - 0.0f, - 0.8f, - 0.0f); - } + public void Draw() + { + _position.Y -= 0.5f; + --_alpha; - public bool IsInvisible => _alpha < 3; + Game1.drawWithBorder( + "Exp " + _experiencePoints, + Color.DarkSlateGray * (_alpha / 100f), + Color.PaleTurquoise * (_alpha / 100f), + Utility.ModifyCoordinatesForUIScale(new Vector2(_position.X - 28, _position.Y - 130)), + 0.0f, + 0.8f, + 0.0f + ); } + } } diff --git a/UIInfoSuite2/UIElements/ExperienceElements/DisplayedLevelUpMessage.cs b/UIInfoSuite2/UIElements/ExperienceElements/DisplayedLevelUpMessage.cs index 61f640d0..f216916e 100644 --- a/UIInfoSuite2/UIElements/ExperienceElements/DisplayedLevelUpMessage.cs +++ b/UIInfoSuite2/UIElements/ExperienceElements/DisplayedLevelUpMessage.cs @@ -4,31 +4,30 @@ namespace UIInfoSuite2.UIElements.ExperienceElements { - public class DisplayedLevelUpMessage + public class DisplayedLevelUpMessage + { + public void Draw(Rectangle levelUpIconRectangle, string levelUpMessage) { - public void Draw(Rectangle levelUpIconRectangle, string levelUpMessage) - { - Vector2 playerLocalPosition = Game1.player.getLocalPosition(Game1.viewport); + Vector2 playerLocalPosition = Game1.player.getLocalPosition(Game1.viewport); - Game1.spriteBatch.Draw( - Game1.mouseCursors, - Utility.ModifyCoordinatesForUIScale(new Vector2( - playerLocalPosition.X - 74, - playerLocalPosition.Y - 130)), levelUpIconRectangle, - Color.White, - 0, - Vector2.Zero, - Game1.pixelZoom, - SpriteEffects.None, - 0.85f); + Game1.spriteBatch.Draw( + Game1.mouseCursors, + Utility.ModifyCoordinatesForUIScale(new Vector2(playerLocalPosition.X - 74, playerLocalPosition.Y - 130)), + levelUpIconRectangle, + Color.White, + 0, + Vector2.Zero, + Game1.pixelZoom, + SpriteEffects.None, + 0.85f + ); - Game1.drawWithBorder( - levelUpMessage, - Color.DarkSlateGray, - Color.PaleTurquoise, - Utility.ModifyCoordinatesForUIScale(new Vector2( - playerLocalPosition.X - 28, - playerLocalPosition.Y - 130))); - } + Game1.drawWithBorder( + levelUpMessage, + Color.DarkSlateGray, + Color.PaleTurquoise, + Utility.ModifyCoordinatesForUIScale(new Vector2(playerLocalPosition.X - 28, playerLocalPosition.Y - 130)) + ); } + } } diff --git a/UIInfoSuite2/UIElements/LocationOfTownsfolk.cs b/UIInfoSuite2/UIElements/LocationOfTownsfolk.cs index a20ccd1c..cbdc2fdc 100644 --- a/UIInfoSuite2/UIElements/LocationOfTownsfolk.cs +++ b/UIInfoSuite2/UIElements/LocationOfTownsfolk.cs @@ -1,412 +1,446 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; -using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Quests; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; +using StardewValley.WorldMaps; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; using UIInfoSuite2.Options; namespace UIInfoSuite2.UIElements { - internal class LocationOfTownsfolk : IDisposable + internal class LocationOfTownsfolk : IDisposable + { + #region Properties + private SocialPage _socialPage = null!; + private string[] _friendNames = null!; + private readonly List _townsfolk = new(); + private readonly List _checkboxes = new(); + + private readonly ModOptions _options; + private readonly IModHelper _helper; + + private const int SocialPanelWidth = 190; + private const int SocialPanelXOffset = 160; + #endregion + + #region Lifecycle + public LocationOfTownsfolk(IModHelper helper, ModOptions options) { - #region Properties - private SocialPage _socialPage; - private string[] _friendNames; - private List _townsfolk = new(); - private List _checkboxes = new(); + _helper = helper; + _options = options; + } - private readonly ModOptions _options; - private readonly IModHelper _helper; + public void ToggleShowNPCLocationsOnMap(bool showLocations) + { + InitializeProperties(); + _helper.Events.Display.MenuChanged -= OnMenuChanged; + _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu_DrawSocialPageOptions; + _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu_DrawNPCLocationsOnMap; + _helper.Events.Input.ButtonPressed -= OnButtonPressed_ForSocialPage; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showLocations) + { + _helper.Events.Display.MenuChanged += OnMenuChanged; + _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu_DrawSocialPageOptions; + _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu_DrawNPCLocationsOnMap; + _helper.Events.Input.ButtonPressed += OnButtonPressed_ForSocialPage; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + } + } - private const int SocialPanelWidth = 190; - private const int SocialPanelXOffset = 160; + public void Dispose() + { + ToggleShowNPCLocationsOnMap(false); + } + #endregion - private static readonly Dictionary> _mapLocations = new() - { - { "HarveyRoom", new KeyValuePair(677, 304) }, - { "BathHouse_Pool", new KeyValuePair(576, 60) }, - { "WizardHouseBasement", new KeyValuePair(196, 352) }, - { "BugLand", new KeyValuePair(0, 0) }, - { "Desert", new KeyValuePair(75, 40) }, - { "Cellar", new KeyValuePair(470, 260) }, - { "JojaMart", new KeyValuePair(872, 280) }, - { "LeoTreeHouse", new KeyValuePair(744, 128) }, - { "Tent", new KeyValuePair(784, 128) }, - { "HaleyHouse", new KeyValuePair(652, 408) }, - { "Hospital", new KeyValuePair(677, 304) }, - { "FarmHouse", new KeyValuePair(470, 260) }, - { "Farm", new KeyValuePair(470, 260) }, - { "ScienceHouse", new KeyValuePair(732, 148) }, - { "ManorHouse", new KeyValuePair(768, 395) }, - { "AdventureGuild", new KeyValuePair(0, 0) }, - { "SeedShop", new KeyValuePair(696, 296) }, - { "Blacksmith", new KeyValuePair(852, 388) }, - { "JoshHouse", new KeyValuePair(740, 320) }, - { "SandyHouse", new KeyValuePair(40, 115) }, - { "Tunnel", new KeyValuePair(0, 0) }, - { "CommunityCenter", new KeyValuePair(692, 204) }, - { "Backwoods", new KeyValuePair(460, 156) }, - { "ElliottHouse", new KeyValuePair(826, 550) }, - { "SebastianRoom", new KeyValuePair(732, 148) }, - { "BathHouse_Entry", new KeyValuePair(576, 60) }, - { "Greenhouse", new KeyValuePair(370, 270) }, - { "Sewer", new KeyValuePair(380, 596) }, - { "WizardHouse", new KeyValuePair(196, 352) }, - { "Trailer", new KeyValuePair(780, 360) }, - { "Trailer_Big", new KeyValuePair(780, 360) }, - { "Forest", new KeyValuePair(80, 272) }, - { "Woods", new KeyValuePair(100, 272) }, - { "WitchSwamp", new KeyValuePair(0, 0) }, - { "ArchaeologyHouse", new KeyValuePair(892, 416) }, - { "FishShop", new KeyValuePair(844, 608) }, - { "Saloon", new KeyValuePair(714, 354) }, - { "LeahHouse", new KeyValuePair(452, 436) }, - { "Town", new KeyValuePair(680, 360) }, - { "Mountain", new KeyValuePair(762, 154) }, - { "BusStop", new KeyValuePair(516, 224) }, - { "Railroad", new KeyValuePair(644, 64) }, - { "SkullCave", new KeyValuePair(0, 0) }, - { "BathHouse_WomensLocker", new KeyValuePair(576, 60) }, - { "Beach", new KeyValuePair(790, 550) }, - { "BathHouse_MensLocker", new KeyValuePair(576, 60) }, - { "Mine", new KeyValuePair(880, 100) }, - { "WitchHut", new KeyValuePair(0, 0) }, - { "AnimalShop", new KeyValuePair(420, 392) }, - { "SamHouse", new KeyValuePair(612, 396) }, - { "WitchWarpCave", new KeyValuePair(0, 0) }, - { "Club", new KeyValuePair(60, 92) }, - { "Sunroom", new KeyValuePair(705, 304) } - }; - #endregion - - #region Lifecycle - public LocationOfTownsfolk(IModHelper helper, ModOptions options) - { - _helper = helper; - _options = options; - } + #region Event subscriptions + private void OnMenuChanged(object? sender, MenuChangedEventArgs e) + { + InitializeProperties(); + } - public void ToggleShowNPCLocationsOnMap(bool showLocations) - { - InitializeProperties(); - _helper.Events.Display.MenuChanged -= OnMenuChanged; - _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu_DrawSocialPageOptions; - _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu_DrawNPCLocationsOnMap; - _helper.Events.Input.ButtonPressed -= OnButtonPressed_ForSocialPage; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - - if (showLocations) - { - _helper.Events.Display.MenuChanged += OnMenuChanged; - _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu_DrawSocialPageOptions; - _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu_DrawNPCLocationsOnMap; - _helper.Events.Input.ButtonPressed += OnButtonPressed_ForSocialPage; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - } - } + private void OnButtonPressed_ForSocialPage(object? sender, ButtonPressedEventArgs e) + { + if (Game1.activeClickableMenu is GameMenu && + e.Button is SButton.MouseLeft or SButton.ControllerA or SButton.ControllerX) + { + CheckSelectedBox(e); + } + } - public void Dispose() - { - ToggleShowNPCLocationsOnMap(false); - } - #endregion + private void OnRenderedActiveMenu_DrawSocialPageOptions(object? sender, RenderedActiveMenuEventArgs e) + { + if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == GameMenu.socialTab) + { + DrawSocialPageOptions(); + } + } - #region Event subscriptions - private void OnMenuChanged(object sender, MenuChangedEventArgs e) - { - InitializeProperties(); - } + private void OnRenderedActiveMenu_DrawNPCLocationsOnMap(object? sender, RenderedActiveMenuEventArgs e) + { + if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == GameMenu.mapTab) + { + DrawNPCLocationsOnMap(gameMenu); + } + } - private void OnButtonPressed_ForSocialPage(object sender, ButtonPressedEventArgs e) - { - if (Game1.activeClickableMenu is GameMenu && (e.Button == SButton.MouseLeft || e.Button == SButton.ControllerA || e.Button == SButton.ControllerX)) - { - CheckSelectedBox(e); - } - } + private void OnUpdateTicked(object? sender, UpdateTickedEventArgs e) + { + if (!e.IsOneSecond || (Context.IsSplitScreen && Context.ScreenId != 0)) + { + return; + } + + _townsfolk.Clear(); - private void OnRenderedActiveMenu_DrawSocialPageOptions(object sender, RenderedActiveMenuEventArgs e) + foreach (GameLocation? loc in Game1.locations) + { + foreach (NPC? character in loc.characters) { - if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 2) - { - DrawSocialPageOptions(); - } + if (character.isVillager()) + { + _townsfolk.Add(character); + } } - private void OnRenderedActiveMenu_DrawNPCLocationsOnMap(object sender, RenderedActiveMenuEventArgs e) + } + } + #endregion + + #region Logic + private void InitializeProperties() + { + if (Game1.activeClickableMenu is GameMenu gameMenu) + { + foreach (IClickableMenu? menu in gameMenu.pages) { - if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 3) - { - DrawNPCLocationsOnMap(gameMenu); - } + if (menu is SocialPage socialPage) + { + _socialPage = socialPage; + _friendNames = socialPage.GetAllNpcs().Select(n => n.Name).ToArray(); + break; + } } - private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + _checkboxes.Clear(); + for (var i = 0; i < _friendNames.Length; i++) { - if (!e.IsOneSecond || (Context.IsSplitScreen && Context.ScreenId != 0)) - return; - - _townsfolk.Clear(); - - foreach (var loc in Game1.locations) - { - foreach (var character in loc.characters) - { - if (character.isVillager()) - _townsfolk.Add(character); - } - } + string friendName = _friendNames[i]; + var checkbox = new OptionsCheckbox("", i); + if (Game1.player.friendshipData.ContainsKey(friendName)) + { + // npc + checkbox.greyedOut = false; + checkbox.isChecked = _options.ShowLocationOfFriends.SafeGet(friendName, true); + } + else + { + // player + checkbox.greyedOut = true; + checkbox.isChecked = true; + } + + _checkboxes.Add(checkbox); } - #endregion + } + } - #region Logic - private void InitializeProperties() + private void CheckSelectedBox(ButtonPressedEventArgs e) + { + var slotPosition = + (int)typeof(SocialPage).GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue( + _socialPage + )!; + + for (int i = slotPosition; i < slotPosition + 5; ++i) + { + OptionsCheckbox checkbox = _checkboxes[i]; + var rect = new Rectangle(checkbox.bounds.X, checkbox.bounds.Y, checkbox.bounds.Width, checkbox.bounds.Height); + if (e.Button == SButton.ControllerX) { - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - foreach (var menu in gameMenu.pages) - { - if (menu is SocialPage socialPage) - { - _socialPage = socialPage; - _friendNames = _socialPage.names - .Select(name => name.ToString()) - .ToArray(); - break; - } - } - - _checkboxes.Clear(); - for (int i = 0; i < _friendNames.Length; i++) - { - var friendName = _friendNames[i]; - OptionsCheckbox checkbox = new OptionsCheckbox("", i); - if (Game1.player.friendshipData.ContainsKey(friendName)) - { - // npc - checkbox.greyedOut = false; - checkbox.isChecked = _options.ShowLocationOfFriends.SafeGet(friendName, true); - } - else - { - // player - checkbox.greyedOut = true; - checkbox.isChecked = true; - } - _checkboxes.Add(checkbox); - } - } + rect.Width += SocialPanelWidth + Game1.activeClickableMenu.width; } - private void CheckSelectedBox(ButtonPressedEventArgs e) + if (rect.Contains( + (int)Utility.ModifyCoordinateForUIScale(Game1.getMouseX()), + (int)Utility.ModifyCoordinateForUIScale(Game1.getMouseY()) + ) && + !checkbox.greyedOut) { - int slotPosition = (int)typeof(SocialPage) - .GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(_socialPage); - - for (int i = slotPosition; i < slotPosition + 5; ++i) - { - OptionsCheckbox checkbox = _checkboxes[i]; - var rect = new Rectangle(checkbox.bounds.X, checkbox.bounds.Y, checkbox.bounds.Width, checkbox.bounds.Height); - if (e.Button == SButton.ControllerX) - { - rect.Width += SocialPanelWidth + Game1.activeClickableMenu.width; - } - if (rect.Contains((int)Utility.ModifyCoordinateForUIScale(Game1.getMouseX()), (int)Utility.ModifyCoordinateForUIScale(Game1.getMouseY())) && - !checkbox.greyedOut) - { - checkbox.isChecked = !checkbox.isChecked; - _options.ShowLocationOfFriends[_friendNames[checkbox.whichOption]] = checkbox.isChecked; - Game1.playSound("drumkit6"); - } - } + checkbox.isChecked = !checkbox.isChecked; + _options.ShowLocationOfFriends[_friendNames[checkbox.whichOption]] = checkbox.isChecked; + Game1.playSound("drumkit6"); } + } + } - private void DrawSocialPageOptions() + private void DrawSocialPageOptions() + { + Game1.drawDialogueBox( + Game1.activeClickableMenu.xPositionOnScreen - SocialPanelXOffset, + Game1.activeClickableMenu.yPositionOnScreen, + SocialPanelWidth, + Game1.activeClickableMenu.height, + false, + true + ); + + var slotPosition = + (int)typeof(SocialPage).GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue( + _socialPage + )!; + var yOffset = 0; + + for (int i = slotPosition; i < slotPosition + 5 && i < _friendNames.Length; ++i) + { + OptionsCheckbox checkbox = _checkboxes[i]; + checkbox.bounds.X = Game1.activeClickableMenu.xPositionOnScreen - 60; + + checkbox.bounds.Y = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; + + checkbox.draw(Game1.spriteBatch, 0, 0); + yOffset += 112; + Color color = checkbox.isChecked ? Color.White : Color.Gray; + + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(checkbox.bounds.X - 50, checkbox.bounds.Y), + new Rectangle(80, 0, 16, 16), + color, + 0.0f, + Vector2.Zero, + 3f, + SpriteEffects.None, + 1f + ); + + if (yOffset != 560) { - Game1.drawDialogueBox(Game1.activeClickableMenu.xPositionOnScreen - SocialPanelXOffset, Game1.activeClickableMenu.yPositionOnScreen, - SocialPanelWidth, Game1.activeClickableMenu.height, false, true); - - int slotPosition = (int)typeof(SocialPage) - .GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(_socialPage); - int yOffset = 0; - - for (int i = slotPosition; i < slotPosition + 5 && i < _friendNames.Length; ++i) - { - OptionsCheckbox checkbox = _checkboxes[i]; - checkbox.bounds.X = Game1.activeClickableMenu.xPositionOnScreen - 60; - - checkbox.bounds.Y = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; - - checkbox.draw(Game1.spriteBatch, 0, 0); - yOffset += 112; - Color color = checkbox.isChecked ? Color.White : Color.Gray; - - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2(checkbox.bounds.X - 50, checkbox.bounds.Y), new Rectangle(80, 0, 16, 16), - color, 0.0f, Vector2.Zero, 3f, SpriteEffects.None, 1f); - - if (yOffset != 560) - { - // draw seperator line - Game1.spriteBatch.Draw(Game1.staminaRect, new Rectangle(checkbox.bounds.X - 50, checkbox.bounds.Y + 72, SocialPanelWidth / 2 - 6, 4), Color.SaddleBrown); - Game1.spriteBatch.Draw(Game1.staminaRect, new Rectangle(checkbox.bounds.X - 50, checkbox.bounds.Y + 76, SocialPanelWidth / 2 - 6, 4), Color.BurlyWood); - } - if (!Game1.options.hardwareCursor) - { - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2(Game1.getMouseX(), Game1.getMouseY()), - Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, Game1.mouseCursor, 16, 16), - Color.White, 0.0f, Vector2.Zero, Game1.pixelZoom + (Game1.dialogueButtonScale / 150.0f), SpriteEffects.None, 1f); - } - - if (checkbox.bounds.Contains(Game1.getMouseX(), Game1.getMouseY())) - IClickableMenu.drawHoverText(Game1.spriteBatch, "Track on map", Game1.dialogueFont); - } + // draw seperator line + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle(checkbox.bounds.X - 50, checkbox.bounds.Y + 72, SocialPanelWidth / 2 - 6, 4), + Color.SaddleBrown + ); + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle(checkbox.bounds.X - 50, checkbox.bounds.Y + 76, SocialPanelWidth / 2 - 6, 4), + Color.BurlyWood + ); } - private void DrawNPCLocationsOnMap(GameMenu gameMenu) + if (!Game1.options.hardwareCursor) { - List namesToShow = new List(); - foreach (var character in _townsfolk) - { - try - { - bool shouldDrawCharacter = Game1.player.friendshipData.ContainsKey(character.Name) && _options.ShowLocationOfFriends.SafeGet(character.Name, true) && character.id != -1; - if (shouldDrawCharacter) - { - DrawNPC(character, namesToShow); - } - } - catch (Exception ex) - { - ModEntry.MonitorObject.Log(ex.Message + Environment.NewLine + ex.StackTrace, LogLevel.Error); - } - } - DrawNPCNames(namesToShow); - - //The cursor needs to show up in front of the character faces - Tools.DrawMouseCursor(); - - string hoverText = (string)typeof(MapPage) - .GetField("hoverText", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(gameMenu.pages[gameMenu.currentTab]); - - IClickableMenu.drawHoverText(Game1.spriteBatch, hoverText, Game1.smallFont); + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(Game1.getMouseX(), Game1.getMouseY()), + Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, Game1.mouseCursor, 16, 16), + Color.White, + 0.0f, + Vector2.Zero, + Game1.pixelZoom + Game1.dialogueButtonScale / 150.0f, + SpriteEffects.None, + 1f + ); } - private static void DrawNPC(NPC character, List namesToShow) + if (checkbox.bounds.Contains(Game1.getMouseX(), Game1.getMouseY())) { - KeyValuePair location = GetMapCoordinatesForNPC(character); - - Rectangle headShot = character.GetHeadShot(); - int xBase = Game1.activeClickableMenu.xPositionOnScreen - 158; - int yBase = Game1.activeClickableMenu.yPositionOnScreen - 40; - - int x = xBase + location.Key; - int y = yBase + location.Value; - - Color color = character.CurrentDialogue.Count <= 0 ? Color.Gray : Color.White; - ClickableTextureComponent textureComponent = new ClickableTextureComponent(character.Name, new Rectangle(x, y, 0, 0), - null, character.Name, character.Sprite.Texture, headShot, 2.3f); - - float headShotScale = 2f; - Game1.spriteBatch.Draw(character.Sprite.Texture, new Vector2(x, y), new Rectangle?(headShot), - color, 0.0f, Vector2.Zero, headShotScale, SpriteEffects.None, 1f); - - int mouseX = Game1.getMouseX(); - int mouseY = Game1.getMouseY(); - if (mouseX >= x && mouseX <= x + headShot.Width * headShotScale && mouseY >= y && mouseY <= y + headShot.Height * headShotScale) - { - namesToShow.Add(character.displayName); - } - - DrawQuestsForNPC(character, x, y); + IClickableMenu.drawHoverText(Game1.spriteBatch, "Track on map", Game1.dialogueFont); } + } + } - private static KeyValuePair GetMapCoordinatesForNPC(NPC character) + private void DrawNPCLocationsOnMap(GameMenu gameMenu) + { + var namesToShow = new List(); + foreach (NPC character in _townsfolk) + { + try { - string locationName = character.currentLocation?.Name ?? character.DefaultMap; - - // Ginger Island - if (character.currentLocation is IslandLocation) - { - return new KeyValuePair(1104, 658); - } - - // Scale Town and Forest - if (locationName == "Town" || locationName == "Forest") - { - int xStart = locationName == "Town" ? 595 : 183; - int yStart = locationName == "Town" ? 163 : 378; - int areaWidth = locationName == "Town" ? 345 : 319; - int areaHeight = locationName == "Town" ? 330 : 261; - - xTile.Map map = character.currentLocation.Map; - - float xScale = areaWidth / (float)map.DisplayWidth; - float yScale = areaHeight / (float)map.DisplayHeight; - - float scaledX = character.position.X * xScale; - float scaledY = character.position.Y * yScale; - int xPos = (int)scaledX + xStart; - int yPos = (int)scaledY + yStart; - return new KeyValuePair(xPos, yPos); - } - - // Other known locations - return _mapLocations.SafeGet(locationName, new KeyValuePair(0, 0)); + bool shouldDrawCharacter = Game1.player.friendshipData.ContainsKey(character.Name) && + _options.ShowLocationOfFriends.SafeGet(character.Name, true) && + character.id != -1; + if (shouldDrawCharacter) + { + DrawNPC(character, namesToShow); + } } - - private static void DrawQuestsForNPC(NPC character, int x, int y) + catch (Exception ex) { - foreach (var quest in Game1.player.questLog.Where(q => q.accepted.Value && q.dailyQuest.Value && !q.completed.Value)) - { - bool isQuestTarget = false; - switch (quest.questType.Value) - { - case 3: isQuestTarget = (quest as ItemDeliveryQuest).target.Value == character.Name; break; - case 4: isQuestTarget = (quest as SlayMonsterQuest).target.Value == character.Name; break; - case 7: isQuestTarget = (quest as FishingQuest).target.Value == character.Name; break; - case 10: isQuestTarget = (quest as ResourceCollectionQuest).target.Value == character.Name; break; - } - - if (isQuestTarget) - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2(x + 10, y - 12), new Rectangle(394, 495, 4, 10), - Color.White, 0.0f, Vector2.Zero, 3f, SpriteEffects.None, 1f); - } + ModEntry.MonitorObject.Log(ex.Message + Environment.NewLine + ex.StackTrace, LogLevel.Error); } + } - private static void DrawNPCNames(List namesToShow) - { - if (namesToShow.Count == 0) - return; + DrawNPCNames(namesToShow); + + //The cursor needs to show up in front of the character faces + Tools.DrawMouseCursor(); - StringBuilder text = new StringBuilder(); - int longestLength = 0; - foreach (string name in namesToShow) - { - text.AppendLine(name); - longestLength = Math.Max(longestLength, (int)Math.Ceiling(Game1.smallFont.MeasureString(name).Length())); - } + string? hoverText = ((MapPage)gameMenu.pages[gameMenu.currentTab]).hoverText; + IClickableMenu.drawHoverText(Game1.spriteBatch, hoverText, Game1.smallFont); + } - int windowHeight = Game1.smallFont.LineSpacing * namesToShow.Count + 25; - Vector2 windowPos = new Vector2(Game1.getMouseX() + 40, Game1.getMouseY() - windowHeight); - IClickableMenu.drawTextureBox(Game1.spriteBatch, (int)windowPos.X, (int)windowPos.Y, - longestLength + 30, Game1.smallFont.LineSpacing * namesToShow.Count + 25, Color.White); + private static void DrawNPC(NPC character, List namesToShow) + { + // Compare with the game code - MapPage.drawMiniPortraits + + Vector2? + location = GetMapCoordinatesForNPC( + character + ); // location is the absolute position or null if the npc is not on the map + if (location is null) + { + return; + } + + Rectangle headShot = character.GetHeadShot(); + MapAreaPosition? mapPosition = + WorldMapManager.GetPositionData( + Game1.player.currentLocation, + new Point((int)location.Value.X, (int)location.Value.Y) + ) ?? + WorldMapManager.GetPositionData(Game1.getFarm(), Point.Zero); + MapRegion? mapRegion = mapPosition.Region; + Rectangle mapBounds = mapRegion.GetMapPixelBounds(); + var offsetLocation = new Vector2( + location.Value.X + mapBounds.X - headShot.Width, + location.Value.Y + mapBounds.Y - headShot.Height + ); + // NOTE! This is the same as the game code, except that where we have 'headShot.Width', the game code has a constant 32. I think that's + // because the player face they draw is 32x32. So we're keeping to the spirit. + + Color color = character.CurrentDialogue.Count <= 0 ? Color.Gray : Color.White; + var headShotScale = 2f; + Game1.spriteBatch.Draw( + character.Sprite.Texture, + offsetLocation, + headShot, + color, + 0.0f, + Vector2.Zero, + headShotScale, + SpriteEffects.None, + 1f + ); + + int mouseX = Game1.getMouseX(); + int mouseY = Game1.getMouseY(); + if (mouseX >= offsetLocation.X && + mouseX - offsetLocation.X <= headShot.Width * headShotScale && + mouseY >= offsetLocation.Y && + mouseY - offsetLocation.Y <= headShot.Height * headShotScale) + { + namesToShow.Add(character.displayName); + } + + DrawQuestsForNPC(character, (int)offsetLocation.X, (int)offsetLocation.Y); + } - Game1.spriteBatch.DrawString(Game1.smallFont, text, new Vector2(windowPos.X + 17, windowPos.Y + 17), Game1.textShadowColor); + private static Vector2? GetMapCoordinatesForNPC(NPC character) + { + var playerNormalizedTile = new Point( + Math.Max(0, Game1.player.TilePoint.X), + Math.Max(0, Game1.player.TilePoint.Y) + ); + MapAreaPosition playerMapAreaPosition = + WorldMapManager.GetPositionData(Game1.player.currentLocation, playerNormalizedTile) ?? + WorldMapManager.GetPositionData(Game1.getFarm(), Point.Zero); + // ^^ Regarding that ?? clause... If the player is in the farmhouse or barn or any building on the farm, GetPositionData is + // going to return null. Thus the fallback to pretending the player is on the farm. However, it seems to me that + // Game1.player.currentLocation.GetParentLocation() would be the safer long-term bet. But rule number 1 of modding is this: + // the game code is always right, even when it's wrong. + + var characterNormalizedTile = new Point(Math.Max(0, character.TilePoint.X), Math.Max(0, character.TilePoint.Y)); + MapAreaPosition characterMapAreaPosition = + WorldMapManager.GetPositionData(character.currentLocation, characterNormalizedTile); + + if (playerMapAreaPosition != null && + characterMapAreaPosition != null && + !(characterMapAreaPosition.Region.Id != playerMapAreaPosition.Region.Id)) + { + return characterMapAreaPosition.GetMapPixelPosition(character.currentLocation, characterNormalizedTile); + } + + return null; + } - Game1.spriteBatch.DrawString(Game1.smallFont, text, new Vector2(windowPos.X + 15, windowPos.Y + 15), Game1.textColor); + private static void DrawQuestsForNPC(NPC character, int x, int y) + { + foreach (Quest? quest in Game1.player.questLog.Where( + q => q.accepted.Value && q.dailyQuest.Value && !q.completed.Value + )) + { + if ((quest is ItemDeliveryQuest idq && idq.target.Value == character.Name) || + (quest is SlayMonsterQuest smq && smq.target.Value == character.Name) || + (quest is FishingQuest fq && fq.target.Value == character.Name) || + (quest is ResourceCollectionQuest rq && rq.target.Value == character.Name)) + { + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(x + 10, y - 12), + new Rectangle(394, 495, 4, 10), + Color.White, + 0.0f, + Vector2.Zero, + 3f, + SpriteEffects.None, + 1f + ); } - #endregion + } + } + + private static void DrawNPCNames(List namesToShow) + { + if (namesToShow.Count == 0) + { + return; + } + + var text = new StringBuilder(); + var longestLength = 0; + foreach (string name in namesToShow) + { + text.AppendLine(name); + longestLength = Math.Max(longestLength, (int)Math.Ceiling(Game1.smallFont.MeasureString(name).Length())); + } + + int windowHeight = Game1.smallFont.LineSpacing * namesToShow.Count + 25; + var windowPos = new Vector2(Game1.getMouseX() + 40, Game1.getMouseY() - windowHeight); + IClickableMenu.drawTextureBox( + Game1.spriteBatch, + (int)windowPos.X, + (int)windowPos.Y, + longestLength + 30, + Game1.smallFont.LineSpacing * namesToShow.Count + 25, + Color.White + ); + + Game1.spriteBatch.DrawString( + Game1.smallFont, + text, + new Vector2(windowPos.X + 17, windowPos.Y + 17), + Game1.textShadowColor + ); + + Game1.spriteBatch.DrawString( + Game1.smallFont, + text, + new Vector2(windowPos.X + 15, windowPos.Y + 15), + Game1.textColor + ); } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/LuckOfDay.cs b/UIInfoSuite2/UIElements/LuckOfDay.cs index dd89d478..6da8e69e 100644 --- a/UIInfoSuite2/UIElements/LuckOfDay.cs +++ b/UIInfoSuite2/UIElements/LuckOfDay.cs @@ -1,181 +1,187 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; -using System; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class LuckOfDay : IDisposable + internal class LuckOfDay : IDisposable + { + #region Properties + private readonly PerScreen _hoverText = new(() => string.Empty); + private readonly PerScreen _color = new(() => new Color(Color.White.ToVector4())); + + private readonly PerScreen _icon = new( + () => new ClickableTextureComponent( + "", + new Rectangle(Tools.GetWidthInPlayArea() - 134, 290, 10 * Game1.pixelZoom, 10 * Game1.pixelZoom), + "", + "", + Game1.mouseCursors, + new Rectangle(50, 428, 10, 14), + Game1.pixelZoom + ) + ); + + private readonly IModHelper _helper; + + private bool Enabled { get; set; } + private bool ShowExactValue { get; set; } + + private static readonly Color Luck1Color = new(87, 255, 106, 255); + private static readonly Color Luck2Color = new(148, 255, 210, 255); + private static readonly Color Luck3Color = new(246, 255, 145, 255); + private static readonly Color Luck4Color = new(255, 255, 255, 255); + private static readonly Color Luck5Color = new(255, 155, 155, 255); + private static readonly Color Luck6Color = new(165, 165, 165, 204); + #endregion + + #region Lifecycle + public LuckOfDay(IModHelper helper) { - #region Properties - private readonly PerScreen _hoverText = new(createNewState: () => string.Empty); - private readonly PerScreen _color = new(createNewState: () => new Color(Color.White.ToVector4())); - private readonly PerScreen _icon = new(createNewState: () => new ClickableTextureComponent("", - new Rectangle(Tools.GetWidthInPlayArea() - 134, - 290, - 10 * Game1.pixelZoom, - 10 * Game1.pixelZoom), - "", - "", - Game1.mouseCursors, - new Rectangle(50, 428, 10, 14), - Game1.pixelZoom, - false)); - private readonly IModHelper _helper; - - private bool Enabled { get; set; } - private bool ShowExactValue { get; set; } - - private static readonly Color Luck1Color = new(87, 255, 106, 255); - private static readonly Color Luck2Color = new(148, 255, 210, 255); - private static readonly Color Luck3Color = new(246, 255, 145, 255); - private static readonly Color Luck4Color = new(255, 255, 255, 255); - private static readonly Color Luck5Color = new(255, 155, 155, 255); - private static readonly Color Luck6Color = new(165, 165, 165, 204); - #endregion - - #region Lifecycle - public LuckOfDay(IModHelper helper) - { - _helper = helper; - } + _helper = helper; + } - public void Dispose() - { - ToggleOption(false); - } + public void Dispose() + { + ToggleOption(false); + } - public void ToggleOption(bool showLuckOfDay) - { - Enabled = showLuckOfDay; - - _helper.Events.Player.Warped -= OnWarped; - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - - if (showLuckOfDay) - { - AdjustIconXToBlackBorder(); - _helper.Events.Player.Warped += OnWarped; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - } - } - public void ToggleShowExactValueOption(bool showExactValue) - { - ShowExactValue = showExactValue; - ToggleOption(Enabled); - } - #endregion + public void ToggleOption(bool showLuckOfDay) + { + Enabled = showLuckOfDay; + + _helper.Events.Player.Warped -= OnWarped; + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showLuckOfDay) + { + AdjustIconXToBlackBorder(); + _helper.Events.Player.Warped += OnWarped; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + } + } - #region Event subscriptions - private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) - { - CalculateLuck(e); - } + public void ToggleShowExactValueOption(bool showExactValue) + { + ShowExactValue = showExactValue; + ToggleOption(Enabled); + } + #endregion - private void OnRenderedHud(object sender, RenderedHudEventArgs e) - { - // draw hover text - if (_icon.Value.containsPoint(Game1.getMouseX(), Game1.getMouseY())) - IClickableMenu.drawHoverText(Game1.spriteBatch, _hoverText.Value, Game1.dialogueFont); - } + #region Event subscriptions + private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + { + CalculateLuck(e); + } - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - // draw dice icon - if (!Game1.eventUp) - { - Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - var icon = _icon.Value; - icon.bounds.X = iconPosition.X; - icon.bounds.Y = iconPosition.Y; - _icon.Value = icon; - _icon.Value.draw(Game1.spriteBatch, _color.Value, 1f); - } - } - #endregion + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + // draw hover text + if (_icon.Value.containsPoint(Game1.getMouseX(), Game1.getMouseY())) + { + IClickableMenu.drawHoverText(Game1.spriteBatch, _hoverText.Value, Game1.dialogueFont); + } + } - #region Logic - private void CalculateLuck(UpdateTickedEventArgs e) - { - if (e.IsMultipleOf(30)) // half second - { - switch (Game1.player.DailyLuck) - { - // Spirits are very happy (FeelingLucky) - case var l when (l > 0.07): - _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus1); - _color.Value = Luck1Color; - break; - // Spirits are in good humor (LuckyButNotTooLucky) - case var l when (l > 0.02 && l <= 0.07): - _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus2); - _color.Value = Luck2Color; - - break; - // The spirits feel neutral - case var l when (l >= -0.02 && l <= 0.02 && l != 0): - _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus3); - _color.Value = Luck3Color; - - break; - // The spirits feel absolutely neutral - case var l when (l == 0): - _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus4); - _color.Value = Luck4Color; - break; - // The spirits are somewhat annoyed (NotFeelingLuckyAtAll) - case var l when (l >= -0.07 && l < -0.02): - _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus5); - _color.Value = Luck5Color; - - break; - // The spirits are very displeased (MaybeStayHome) - case var l when (l < -0.07): - _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus6); - _color.Value = Luck6Color; - break; - } - - // Rewrite the text, but keep the color - if (ShowExactValue) - { - _hoverText.Value = string.Format(_helper.SafeGetString(LanguageKeys.DailyLuckValue), Game1.player.DailyLuck.ToString("N3")); - } - } - } + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + // draw dice icon + if (!Game1.eventUp) + { + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + ClickableTextureComponent icon = _icon.Value; + icon.bounds.X = iconPosition.X; + icon.bounds.Y = iconPosition.Y; + _icon.Value = icon; + _icon.Value.draw(Game1.spriteBatch, _color.Value, 1f); + } + } + #endregion - private void OnWarped(object sender, WarpedEventArgs e) + #region Logic + private void CalculateLuck(UpdateTickedEventArgs e) + { + if (e.IsMultipleOf(30)) // half second + { + switch (Game1.player.DailyLuck) { - // adjust icon X to black border - if (e.IsLocalPlayer) - { - AdjustIconXToBlackBorder(); - } + // Spirits are very happy (FeelingLucky) + case var l when l > 0.07: + _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus1); + _color.Value = Luck1Color; + break; + // Spirits are in good humor (LuckyButNotTooLucky) + case var l when l > 0.02 && l <= 0.07: + _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus2); + _color.Value = Luck2Color; + + break; + // The spirits feel neutral + case var l when l >= -0.02 && l <= 0.02 && l != 0: + _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus3); + _color.Value = Luck3Color; + + break; + // The spirits feel absolutely neutral + case var l when l == 0: + _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus4); + _color.Value = Luck4Color; + break; + // The spirits are somewhat annoyed (NotFeelingLuckyAtAll) + case var l when l >= -0.07 && l < -0.02: + _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus5); + _color.Value = Luck5Color; + + break; + // The spirits are very displeased (MaybeStayHome) + case var l when l < -0.07: + _hoverText.Value = _helper.SafeGetString(LanguageKeys.LuckStatus6); + _color.Value = Luck6Color; + break; } - private void AdjustIconXToBlackBorder() + // Rewrite the text, but keep the color + if (ShowExactValue) { - _icon.Value = new ClickableTextureComponent("", - new Rectangle(Tools.GetWidthInPlayArea() - 134, - 290, - 10 * Game1.pixelZoom, - 10 * Game1.pixelZoom), - "", - "", - Game1.mouseCursors, - new Rectangle(50, 428, 10, 14), - Game1.pixelZoom, - false); + _hoverText.Value = string.Format( + _helper.SafeGetString(LanguageKeys.DailyLuckValue), + Game1.player.DailyLuck.ToString("N3") + ); } - #endregion + } + } + + private void OnWarped(object sender, WarpedEventArgs e) + { + // adjust icon X to black border + if (e.IsLocalPlayer) + { + AdjustIconXToBlackBorder(); + } + } + + private void AdjustIconXToBlackBorder() + { + _icon.Value = new ClickableTextureComponent( + "", + new Rectangle(Tools.GetWidthInPlayArea() - 134, 290, 10 * Game1.pixelZoom, 10 * Game1.pixelZoom), + "", + "", + Game1.mouseCursors, + new Rectangle(50, 428, 10, 14), + Game1.pixelZoom + ); } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShopHarvestPrices.cs b/UIInfoSuite2/UIElements/ShopHarvestPrices.cs index 02f1f9e3..b9b32e7d 100644 --- a/UIInfoSuite2/UIElements/ShopHarvestPrices.cs +++ b/UIInfoSuite2/UIElements/ShopHarvestPrices.cs @@ -1,129 +1,139 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShopHarvestPrices : IDisposable + internal class ShopHarvestPrices : IDisposable + { + private readonly IModHelper _helper; + + public ShopHarvestPrices(IModHelper helper) { - private readonly IModHelper _helper; + _helper = helper; + } - public ShopHarvestPrices(IModHelper helper) - { - _helper = helper; - } + public void Dispose() + { + ToggleOption(false); + } - public void ToggleOption(bool shopHarvestPrices) - { - _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; + public void ToggleOption(bool shopHarvestPrices) + { + _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; - if (shopHarvestPrices) - { - _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu; - } - } + if (shopHarvestPrices) + { + _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu; + } + } - public void Dispose() - { - ToggleOption(false); - } + /// + /// When a menu is open ( isn't null), raised after that menu is drawn to + /// the sprite batch but before it's rendered to the screen. + /// + /// The event sender. + /// The event arguments. + private void OnRenderedActiveMenu(object sender, RenderedActiveMenuEventArgs e) + { + if (!(Game1.activeClickableMenu is ShopMenu menu)) + { + return; + } - /// When a menu is open ( isn't null), raised after that menu is drawn to the sprite batch but before it's rendered to the screen. - /// The event sender. - /// The event arguments. - private void OnRenderedActiveMenu(object sender, RenderedActiveMenuEventArgs e) - { - if (!(Game1.activeClickableMenu is ShopMenu menu)) return; - if (!(menu.hoveredItem is Item hoverItem)) return; + if (!(menu.hoveredItem is Item hoverItem)) + { + return; + } - // draw shop harvest prices - int value = Tools.GetHarvestPrice(hoverItem); + // draw shop harvest prices + int value = Tools.GetHarvestPrice(hoverItem); - if (value > 0) - { - int xPosition = menu.xPositionOnScreen - 30; - int yPosition = menu.yPositionOnScreen + 580; - IClickableMenu.drawTextureBox( - Game1.spriteBatch, - xPosition + 20, - yPosition - 52, - 264, - 108, - Color.White); - // Title "Harvest Price" - string textToRender = _helper.SafeGetString(LanguageKeys.HarvestPrice); - Game1.spriteBatch.DrawString( - Game1.dialogueFont, - textToRender, - new Vector2(xPosition + 30, yPosition - 38), - Color.Black * 0.2f); - Game1.spriteBatch.DrawString( - Game1.dialogueFont, - textToRender, - new Vector2(xPosition + 32, yPosition - 40), - Color.Black * 0.8f); - // Tree Icon - xPosition += 80; - Game1.spriteBatch.Draw( - Game1.mouseCursors, - new Vector2(xPosition, yPosition), - new Rectangle(60, 428, 10, 10), - Color.White, - 0, - Vector2.Zero, - Game1.pixelZoom, - SpriteEffects.None, - 0.85f); - // Coin - Game1.spriteBatch.Draw( - Game1.debrisSpriteSheet, - new Vector2(xPosition + 32, yPosition + 10), - Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), - Color.White, - 0, - new Vector2(8, 8), - 4, - SpriteEffects.None, - 0.95f); - // Price - Game1.spriteBatch.DrawString( - Game1.dialogueFont, - value.ToString(), - new Vector2(xPosition + 50, yPosition + 6), - Color.Black * 0.2f); - Game1.spriteBatch.DrawString( - Game1.dialogueFont, - value.ToString(), - new Vector2(xPosition + 52, yPosition + 4), - Color.Black * 0.8f); - /* - * I have no Idea why this is here... - * As far as I can see it only overrides the existing Tooltip with a price that is 500 coins higher? - * - string hoverText = _helper.Reflection.GetField(menu, "hoverText").GetValue(); - string hoverTitle = _helper.Reflection.GetField(menu, "boldTitleText").GetValue(); - IReflectedMethod getHoveredItemExtraItemIndex = _helper.Reflection.GetMethod(menu, "getHoveredItemExtraItemIndex"); - IReflectedMethod getHoveredItemExtraItemAmount = _helper.Reflection.GetMethod(menu, "getHoveredItemExtraItemAmount"); - IClickableMenu.drawToolTip( - Game1.spriteBatch, - hoverText, - hoverTitle, - hoverItem, - menu.heldItem != null, - -1, - menu.currency, - getHoveredItemExtraItemIndex.Invoke(new object[0]), - getHoveredItemExtraItemAmount.Invoke(new object[0]), - null, - menu.hoverPrice); - */ - } - } + if (value > 0) + { + int xPosition = menu.xPositionOnScreen - 30; + int yPosition = menu.yPositionOnScreen + 580; + IClickableMenu.drawTextureBox(Game1.spriteBatch, xPosition + 20, yPosition - 52, 264, 108, Color.White); + // Title "Harvest Price" + string textToRender = _helper.SafeGetString(LanguageKeys.HarvestPrice); + Game1.spriteBatch.DrawString( + Game1.dialogueFont, + textToRender, + new Vector2(xPosition + 30, yPosition - 38), + Color.Black * 0.2f + ); + Game1.spriteBatch.DrawString( + Game1.dialogueFont, + textToRender, + new Vector2(xPosition + 32, yPosition - 40), + Color.Black * 0.8f + ); + // Tree Icon + xPosition += 80; + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(xPosition, yPosition), + new Rectangle(60, 428, 10, 10), + Color.White, + 0, + Vector2.Zero, + Game1.pixelZoom, + SpriteEffects.None, + 0.85f + ); + // Coin + Game1.spriteBatch.Draw( + Game1.debrisSpriteSheet, + new Vector2(xPosition + 32, yPosition + 10), + Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), + Color.White, + 0, + new Vector2(8, 8), + 4, + SpriteEffects.None, + 0.95f + ); + // Price + Game1.spriteBatch.DrawString( + Game1.dialogueFont, + value.ToString(), + new Vector2(xPosition + 50, yPosition + 6), + Color.Black * 0.2f + ); + Game1.spriteBatch.DrawString( + Game1.dialogueFont, + value.ToString(), + new Vector2(xPosition + 52, yPosition + 4), + Color.Black * 0.8f + ); + /* + * I have no Idea why this is here... + * As far as I can see it only overrides the existing Tooltip with a price that is 500 coins higher? + * + string hoverText = _helper.Reflection.GetField(menu, "hoverText").GetValue(); + string hoverTitle = _helper.Reflection.GetField(menu, "boldTitleText").GetValue(); + IReflectedMethod getHoveredItemExtraItemIndex = _helper.Reflection.GetMethod(menu, "getHoveredItemExtraItemIndex"); + IReflectedMethod getHoveredItemExtraItemAmount = _helper.Reflection.GetMethod(menu, "getHoveredItemExtraItemAmount"); + IClickableMenu.drawToolTip( + Game1.spriteBatch, + hoverText, + hoverTitle, + hoverItem, + menu.heldItem != null, + -1, + menu.currency, + getHoveredItemExtraItemIndex.Invoke(new object[0]), + getHoveredItemExtraItemAmount.Invoke(new object[0]), + null, + menu.hoverPrice); + */ + } } + } } diff --git a/UIInfoSuite2/UIElements/ShowAccurateHearts.cs b/UIInfoSuite2/UIElements/ShowAccurateHearts.cs index 8d586cfa..2b616fd0 100644 --- a/UIInfoSuite2/UIElements/ShowAccurateHearts.cs +++ b/UIInfoSuite2/UIElements/ShowAccurateHearts.cs @@ -1,157 +1,159 @@ -using Microsoft.Xna.Framework; +using System; +using System.Reflection; +using Microsoft.Xna.Framework; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; -using System.Linq; -using System.Reflection; namespace UIInfoSuite2.UIElements { - internal class ShowAccurateHearts : IDisposable + internal class ShowAccurateHearts : IDisposable + { + #region Properties + private SocialPage? _socialPage; + private readonly IModEvents _events; + + // @formatter:off + private readonly int[][] _numArray = { - #region Properties - private string[] _friendNames; - private SocialPage _socialPage; - private IModEvents _events; + new[] { 1, 1, 0, 1, 1 }, + new[] { 1, 1, 1, 1, 1 }, + new[] { 0, 1, 1, 1, 0 }, + new[] { 0, 0, 1, 0, 0 } + }; + // @formatter:on + #endregion + + #region Lifecycle + public ShowAccurateHearts(IModEvents events) + { + _events = events; + } - private readonly int[][] _numArray = new int[][] - { - new int[] { 1, 1, 0, 1, 1 }, - new int[] { 1, 1, 1, 1, 1 }, - new int[] { 0, 1, 1, 1, 0 }, - new int[] { 0, 0, 1, 0, 0 } - }; - #endregion - - #region Lifecycle - public ShowAccurateHearts(IModEvents events) - { - _events = events; - } + public void Dispose() + { + ToggleOption(false); + } - public void Dispose() - { - ToggleOption(false); - } + public void ToggleOption(bool showAccurateHearts) + { + _events.Display.MenuChanged -= OnMenuChanged; + _events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; + + if (showAccurateHearts) + { + _events.Display.MenuChanged += OnMenuChanged; + _events.Display.RenderedActiveMenu += OnRenderedActiveMenu; + } + } + #endregion - public void ToggleOption(bool showAccurateHearts) - { - _events.Display.MenuChanged -= OnMenuChanged; - _events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; - - if (showAccurateHearts) - { - _events.Display.MenuChanged += OnMenuChanged; - _events.Display.RenderedActiveMenu += OnRenderedActiveMenu; - } - } - #endregion + #region Event subscriptions + private void OnRenderedActiveMenu(object sender, RenderedActiveMenuEventArgs e) + { + if (_socialPage == null) + { + GetSocialPage(); + return; + } + + if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 2) + { + DrawHeartFills(); + + string hoverText = gameMenu.hoverText; + IClickableMenu.drawHoverText(Game1.spriteBatch, hoverText, Game1.smallFont); + } + } - #region Event subscriptions - private void OnRenderedActiveMenu(object sender, RenderedActiveMenuEventArgs e) - { - if (_socialPage == null) - { - ExtendMenuIfNeeded(); - return; - } - - if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 2) - { - DrawHeartFills(); - - string hoverText = gameMenu.hoverText; - IClickableMenu.drawHoverText( - Game1.spriteBatch, - hoverText, - Game1.smallFont); - } - } + private void OnMenuChanged(object sender, MenuChangedEventArgs e) + { + GetSocialPage(); + } + #endregion - private void OnMenuChanged(object sender, MenuChangedEventArgs e) + #region Logic + private void GetSocialPage() + { + if (Game1.activeClickableMenu is GameMenu gameMenu) + { + foreach (IClickableMenu? menu in gameMenu.pages) { - ExtendMenuIfNeeded(); + if (menu is SocialPage page) + { + _socialPage = page; + break; + } } - #endregion + } + } - #region Logic - private void ExtendMenuIfNeeded() + private void DrawHeartFills() + { + if (_socialPage == null) + { + return; + } + + + var slotPosition = + (int)typeof(SocialPage).GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue( + _socialPage + )!; + var yOffset = 0; + + for (int i = slotPosition; i < slotPosition + 5 && i < _socialPage.SocialEntries.Count; ++i) + { + string internalName = _socialPage.SocialEntries[i].InternalName; + if (Game1.player.friendshipData.TryGetValue(internalName, out Friendship friendshipValues) + && friendshipValues.Points > 0 + && friendshipValues.Points < Utility.GetMaximumHeartsForCharacter(Game1.getCharacterFromName(internalName)) * 250) { - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - foreach (var menu in gameMenu.pages) - { - if (menu is SocialPage page) - { - _socialPage = page; - _friendNames = _socialPage.names - .Select(name => name.ToString()) - .ToArray(); - break; - } - } - } + int pointsToNextHeart = friendshipValues.Points % 250; + int numHearts = friendshipValues.Points / 250; + int yPosition = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; + DrawEachIndividualSquare(numHearts, pointsToNextHeart, yPosition); } - private void DrawHeartFills() - { - int slotPosition = (int)typeof(SocialPage) - .GetField( - "slotPosition", - BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(_socialPage); - int yOffset = 0; - - for (int i = slotPosition; i < slotPosition + 5 && i < _friendNames.Length; ++i) - { - if (Game1.player.friendshipData.TryGetValue(_friendNames[i], out Friendship friendshipValues) - && friendshipValues.Points > 0 - && friendshipValues.Points < Utility.GetMaximumHeartsForCharacter(Game1.getCharacterFromName(_friendNames[i])) * 250) - { - int pointsToNextHeart = friendshipValues.Points % 250; - int numHearts = friendshipValues.Points / 250; - int yPosition = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; - DrawEachIndividualSquare(numHearts, pointsToNextHeart, yPosition); - } - - yOffset += 112; - } - } + yOffset += 112; + } + } - private void DrawEachIndividualSquare(int friendshipLevel, int friendshipPoints, int yPosition) + private void DrawEachIndividualSquare(int friendshipLevel, int friendshipPoints, int yPosition) + { + var numberOfPointsToDraw = (int)(friendshipPoints / 12.5); + int num2; + + if (friendshipLevel >= 10) + { + num2 = 32 * (friendshipLevel - 10); + yPosition += 28; + } + else + { + num2 = 32 * friendshipLevel; + } + + for (var i = 3; i >= 0 && numberOfPointsToDraw > 0; --i) + { + for (var j = 0; j < 5 && numberOfPointsToDraw > 0; ++j, --numberOfPointsToDraw) { - int numberOfPointsToDraw = (int)((friendshipPoints) / 12.5); - int num2; - - if (friendshipLevel >= 10) - { - num2 = 32 * (friendshipLevel - 10); - yPosition += 28; - } - else - { - num2 = 32 * friendshipLevel; - } - - for (int i = 3; i >= 0 && numberOfPointsToDraw > 0; --i) - { - for (int j = 0; j < 5 && numberOfPointsToDraw > 0; ++j, --numberOfPointsToDraw) - { - if (_numArray[i][j] == 1) - { - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle( - Game1.activeClickableMenu.xPositionOnScreen + 320 + num2 + j * 4, - yPosition + 14 + i * 4, - 4, - 4), - Color.Crimson); - } - } - } + if (_numArray[i][j] == 1) + { + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle( + Game1.activeClickableMenu.xPositionOnScreen + 320 + num2 + j * 4, + yPosition + 14 + i * 4, + 4, + 4 + ), + Color.Crimson + ); + } } - #endregion + } } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowBirthdayIcon.cs b/UIInfoSuite2/UIElements/ShowBirthdayIcon.cs index effb9081..5de50d32 100644 --- a/UIInfoSuite2/UIElements/ShowBirthdayIcon.cs +++ b/UIInfoSuite2/UIElements/ShowBirthdayIcon.cs @@ -1,217 +1,221 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; -using System; -using System.Collections.Generic; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowBirthdayIcon : IDisposable - { - #region Properties - private readonly PerScreen> _birthdayNPCs = new(() => new()); - private readonly PerScreen> _birthdayIcons = new(() => new()); + internal class ShowBirthdayIcon : IDisposable + { + #region Properties + private readonly PerScreen> _birthdayNPCs = new(() => new List()); - private bool Enabled { get; set; } - private bool HideBirthdayIfFullFriendShip { get; set; } - private readonly IModHelper _helper; - #endregion + private readonly PerScreen> _birthdayIcons = + new(() => new List()); + private bool Enabled { get; set; } + private bool HideBirthdayIfFullFriendShip { get; set; } + private readonly IModHelper _helper; + #endregion - #region Life cycle - public ShowBirthdayIcon(IModHelper helper) - { - _helper = helper; - } - public void Dispose() - { - ToggleOption(false); - } + #region Life cycle + public ShowBirthdayIcon(IModHelper helper) + { + _helper = helper; + } - public void ToggleOption(bool showBirthdayIcon) - { - Enabled = showBirthdayIcon; + public void Dispose() + { + ToggleOption(false); + } - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + public void ToggleOption(bool showBirthdayIcon) + { + Enabled = showBirthdayIcon; + + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showBirthdayIcon) + { + CheckForBirthday(); + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + } + } - if (showBirthdayIcon) - { - CheckForBirthday(); - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - } - } + public void ToggleDisableOnMaxFriendshipOption(bool hideBirthdayIfFullFriendShip) + { + HideBirthdayIfFullFriendShip = hideBirthdayIfFullFriendShip; + ToggleOption(Enabled); + } + #endregion - public void ToggleDisableOnMaxFriendshipOption(bool hideBirthdayIfFullFriendShip) - { - HideBirthdayIfFullFriendShip = hideBirthdayIfFullFriendShip; - ToggleOption(Enabled); - } - #endregion + #region Event subscriptions + private void OnUpdateTicked(object? sender, UpdateTickedEventArgs e) + { + if (e.IsOneSecond) + { + CheckForGiftGiven(); + } + } + private void OnDayStarted(object? sender, DayStartedEventArgs e) + { + CheckForBirthday(); + } - #region Event subscriptions - private void OnUpdateTicked(object? sender, UpdateTickedEventArgs e) - { - if (e.IsOneSecond) - { - CheckForGiftGiven(); - } - } + private void OnRenderingHud(object? sender, RenderingHudEventArgs e) + { + if (!Game1.eventUp) + { + DrawBirthdayIcon(); + } + } - private void OnDayStarted(object? sender, DayStartedEventArgs e) - { - CheckForBirthday(); - } - private void OnRenderingHud(object? sender, RenderingHudEventArgs e) - { - if (!Game1.eventUp) - { - DrawBirthdayIcon(); - } - } + private void OnRenderedHud(object? sender, RenderedHudEventArgs e) + { + if (!Game1.eventUp) + { + DrawHoverText(); + } + } + #endregion - private void OnRenderedHud(object? sender, RenderedHudEventArgs e) + #region Logic + private void CheckForGiftGiven() + { + List npcs = _birthdayNPCs.Value; + // Iterate from the end so that removing items doesn't affect indices + for (int i = npcs.Count - 1; i >= 0; i--) + { + Friendship? friendship = GetFriendshipWithNPC(npcs[i].Name); + if (friendship != null && friendship.GiftsToday > 0) { - if (!Game1.eventUp) - { - DrawHoverText(); - } + npcs.RemoveAt(i); } - #endregion - + } + } - #region Logic - private void CheckForGiftGiven() + private void CheckForBirthday() + { + _birthdayNPCs.Value.Clear(); + foreach (GameLocation? location in Game1.locations) + { + foreach (NPC? character in location.characters) { - List npcs = _birthdayNPCs.Value; - // Iterate from the end so that removing items doesn't affect indices - for (int i = npcs.Count - 1; i >= 0; i--) + if (character.isBirthday()) + { + Friendship? friendship = GetFriendshipWithNPC(character.Name); + if (friendship != null) { - Friendship? friendship = GetFriendshipWithNPC(npcs[i].Name); - if (friendship != null && friendship.GiftsToday > 0) - { - npcs.RemoveAt(i); - } + if (HideBirthdayIfFullFriendShip && + friendship.Points >= + Utility.GetMaximumHeartsForCharacter(character) * NPC.friendshipPointsPerHeartLevel) + { + continue; + } + + _birthdayNPCs.Value.Add(character); } + } } + } + } - private void CheckForBirthday() + private static Friendship? GetFriendshipWithNPC(string name) + { + try + { + if (Game1.player.friendshipData.TryGetValue(name, out Friendship friendship)) { - _birthdayNPCs.Value.Clear(); - foreach (var location in Game1.locations) - { - foreach (var character in location.characters) - { - if (character.isBirthday(Game1.currentSeason, Game1.dayOfMonth)) - { - Friendship? friendship = GetFriendshipWithNPC(character.Name); - if (friendship != null) - { - if (HideBirthdayIfFullFriendShip && friendship.Points >= Utility.GetMaximumHeartsForCharacter(character) * NPC.friendshipPointsPerHeartLevel) - continue; - - _birthdayNPCs.Value.Add(character); - } - - } - } - } + return friendship; } - private static Friendship? GetFriendshipWithNPC(string name) - { - try - { - if (Game1.player.friendshipData.TryGetValue(name, out Friendship friendship)) - return friendship; - else - return null; - } - catch (Exception ex) - { - ModEntry.MonitorObject.LogOnce("Error while getting information about the birthday of " + name, LogLevel.Error); - ModEntry.MonitorObject.Log(ex.ToString()); - } + return null; + } + catch (Exception ex) + { + ModEntry.MonitorObject.LogOnce("Error while getting information about the birthday of " + name, LogLevel.Error); + ModEntry.MonitorObject.Log(ex.ToString()); + } - return null; - } + return null; + } - private void DrawBirthdayIcon() - { - _birthdayIcons.Value.Clear(); - foreach (var npc in _birthdayNPCs.Value) - { - Rectangle headShot = npc.GetHeadShot(); - Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - float scale = 2.9f; - - Game1.spriteBatch.Draw( - Game1.mouseCursors, - new Vector2(iconPosition.X, iconPosition.Y), - new Rectangle(228, 409, 16, 16), - Color.White, - 0.0f, - Vector2.Zero, - scale, - SpriteEffects.None, - 1f); - - var birthdayIcon = new ClickableTextureComponent( - npc.Name, - new Rectangle( - iconPosition.X - 7, - iconPosition.Y - 2, - (int)(16.0 * scale), - (int)(16.0 * scale)), - null, - npc.Name, - npc.Sprite.Texture, - headShot, - 2f); - - birthdayIcon.draw(Game1.spriteBatch); - _birthdayIcons.Value.Add(birthdayIcon); - } - } + private void DrawBirthdayIcon() + { + _birthdayIcons.Value.Clear(); + foreach (NPC npc in _birthdayNPCs.Value) + { + Rectangle headShot = npc.GetHeadShot(); + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + var scale = 2.9f; + + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(iconPosition.X, iconPosition.Y), + new Rectangle(228, 409, 16, 16), + Color.White, + 0.0f, + Vector2.Zero, + scale, + SpriteEffects.None, + 1f + ); + + var birthdayIcon = new ClickableTextureComponent( + npc.Name, + new Rectangle(iconPosition.X - 7, iconPosition.Y - 2, (int)(16.0 * scale), (int)(16.0 * scale)), + null, + npc.Name, + npc.Sprite.Texture, + headShot, + 2f + ); + + birthdayIcon.draw(Game1.spriteBatch); + _birthdayIcons.Value.Add(birthdayIcon); + } + } - private void DrawHoverText() + private void DrawHoverText() + { + List icons = _birthdayIcons.Value; + List npcs = _birthdayNPCs.Value; + if (icons.Count != npcs.Count) + { + ModEntry.MonitorObject.LogOnce( + $"{GetType().Name}: The number of tracked npcs and icons do not match", + LogLevel.Error + ); + return; + } + + for (var i = 0; i < icons.Count; i++) + { + if (icons[i].containsPoint(Game1.getMouseX(), Game1.getMouseY())) { - var icons = _birthdayIcons.Value; - var npcs = _birthdayNPCs.Value; - if (icons.Count != npcs.Count) - { - ModEntry.MonitorObject.LogOnce($"{this.GetType().Name}: The number of tracked npcs and icons do not match", LogLevel.Error); - return; - } - - for (int i = 0; i < icons.Count; i++) - { - if (icons[i].containsPoint(Game1.getMouseX(), Game1.getMouseY())) - { - string hoverText = string.Format(_helper.SafeGetString(LanguageKeys.NpcBirthday), npcs[i].displayName); - IClickableMenu.drawHoverText( - Game1.spriteBatch, - hoverText, - Game1.dialogueFont); - } - } + string hoverText = string.Format(_helper.SafeGetString(LanguageKeys.NpcBirthday), npcs[i].displayName); + IClickableMenu.drawHoverText(Game1.spriteBatch, hoverText, Game1.dialogueFont); } - #endregion + } } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowCalendarAndBillboardOnGameMenuButton.cs b/UIInfoSuite2/UIElements/ShowCalendarAndBillboardOnGameMenuButton.cs index 4fd62f89..1c7e4f8d 100644 --- a/UIInfoSuite2/UIElements/ShowCalendarAndBillboardOnGameMenuButton.cs +++ b/UIInfoSuite2/UIElements/ShowCalendarAndBillboardOnGameMenuButton.cs @@ -1,141 +1,153 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; -using System; -using System.Collections.Generic; -using System.IO; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowCalendarAndBillboardOnGameMenuButton : IDisposable + internal class ShowCalendarAndBillboardOnGameMenuButton : IDisposable + { + #region Properties + private readonly PerScreen _showBillboardButton = new( + () => new ClickableTextureComponent( + new Rectangle(0, 0, 99, 60), + Game1.content.Load(Path.Combine("Maps", "summer_town")), + new Rectangle(122, 291, 35, 20), + 3f + ) + ); + + private readonly IModHelper _helper; + + private readonly PerScreen _hoverItem = new(); + private readonly PerScreen _heldItem = new(); + #endregion + + #region Lifecycle + public ShowCalendarAndBillboardOnGameMenuButton(IModHelper helper) { - #region Properties - private readonly PerScreen _showBillboardButton = new(createNewState: () => - new ClickableTextureComponent( - new Rectangle(0, 0, 99, 60), - Game1.content.Load(Path.Combine("Maps", "summer_town")), - new Rectangle(122, 291, 35, 20), - 3f)); - - private readonly IModHelper _helper; - - private readonly PerScreen _hoverItem = new(); - private readonly PerScreen _heldItem = new(); - #endregion - - #region Lifecycle - public ShowCalendarAndBillboardOnGameMenuButton(IModHelper helper) - { - _helper = helper; - } + _helper = helper; + } - public void Dispose() - { - ToggleOption(false); - } + public void Dispose() + { + ToggleOption(false); + } - public void ToggleOption(bool showCalendarAndBillboard) - { - ModEntry.RegisterCalendarAndQuestKeyBindings(_helper, showCalendarAndBillboard); - - _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; - _helper.Events.Input.ButtonPressed -= OnButtonPressed; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - - if (showCalendarAndBillboard) - { - _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu; - _helper.Events.Input.ButtonPressed += OnButtonPressed; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - } - } - #endregion + public void ToggleOption(bool showCalendarAndBillboard) + { + ModEntry.RegisterCalendarAndQuestKeyBindings(_helper, showCalendarAndBillboard); + + _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; + _helper.Events.Input.ButtonPressed -= OnButtonPressed; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showCalendarAndBillboard) + { + _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu; + _helper.Events.Input.ButtonPressed += OnButtonPressed; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + } + } + #endregion - #region Event subscriptions - private void OnUpdateTicked(object sender, EventArgs e) - { - // Get hovered and hold item - _hoverItem.Value = Tools.GetHoveredItem(); - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - List menuList = gameMenu.pages; - - if (menuList[0] is InventoryPage inventory) - { - _heldItem.Value = Game1.player.CursorSlotItem; - } - } - } + #region Event subscriptions + private void OnUpdateTicked(object sender, EventArgs e) + { + // Get hovered and hold item + _hoverItem.Value = Tools.GetHoveredItem(); + if (Game1.activeClickableMenu is GameMenu gameMenu) + { + List menuList = gameMenu.pages; - private void OnButtonPressed(object sender, ButtonPressedEventArgs e) + if (menuList[0] is InventoryPage inventory) { - if (e.Button == SButton.MouseLeft) - ActivateBillboard(); - else if (e.Button == SButton.ControllerA) - ActivateBillboard(); + _heldItem.Value = Game1.player.CursorSlotItem; } + } + } - private void OnRenderedActiveMenu(object sender, EventArgs e) - { - if (_hoverItem.Value == null - && Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 0 - && _heldItem.Value == null) - { - DrawBillboard(); - } - } - #endregion + private void OnButtonPressed(object sender, ButtonPressedEventArgs e) + { + if (e.Button == SButton.MouseLeft) + { + ActivateBillboard(); + } + else if (e.Button == SButton.ControllerA) + { + ActivateBillboard(); + } + } + + private void OnRenderedActiveMenu(object sender, EventArgs e) + { + if (_hoverItem.Value == null && + Game1.activeClickableMenu is GameMenu gameMenu && + gameMenu.currentTab == 0 && + _heldItem.Value == null) + { + DrawBillboard(); + } + } + #endregion - #region Logic - private void DrawBillboard() - { - var billboardButton = _showBillboardButton.Value; - billboardButton.bounds.X = Game1.activeClickableMenu.xPositionOnScreen + Game1.activeClickableMenu.width - 160; - billboardButton.bounds.Y = Game1.activeClickableMenu.yPositionOnScreen + Game1.activeClickableMenu.height - - // For compatiblity with BiggerBackpack mod - (_helper.ModRegistry.IsLoaded("spacechase0.BiggerBackpack") ? 230 : 300); - - _showBillboardButton.Value = billboardButton; - _showBillboardButton.Value.draw(Game1.spriteBatch); - - // Draw the mouse again to display it over the billboard - Game1.activeClickableMenu.drawMouse(Game1.spriteBatch); - - if (_showBillboardButton.Value.containsPoint(Game1.getMouseX(), Game1.getMouseY())) - { - string hoverText = Game1.getMouseX() < - _showBillboardButton.Value.bounds.X + _showBillboardButton.Value.bounds.Width / 2 ? - LanguageKeys.Calendar : LanguageKeys.Billboard; - IClickableMenu.drawHoverText( - Game1.spriteBatch, - _helper.SafeGetString(hoverText), - Game1.dialogueFont); - } - } + #region Logic + private void DrawBillboard() + { + ClickableTextureComponent billboardButton = _showBillboardButton.Value; + billboardButton.bounds.X = Game1.activeClickableMenu.xPositionOnScreen + Game1.activeClickableMenu.width - 160; + billboardButton.bounds.Y = Game1.activeClickableMenu.yPositionOnScreen + + Game1.activeClickableMenu.height - + // For compatiblity with BiggerBackpack mod + (_helper.ModRegistry.IsLoaded("spacechase0.BiggerBackpack") ? 230 : 300); + + _showBillboardButton.Value = billboardButton; + _showBillboardButton.Value.draw(Game1.spriteBatch); + + // Draw the mouse again to display it over the billboard + Game1.activeClickableMenu.drawMouse(Game1.spriteBatch); + + if (_showBillboardButton.Value.containsPoint(Game1.getMouseX(), Game1.getMouseY())) + { + string hoverText = Game1.getMouseX() < + _showBillboardButton.Value.bounds.X + _showBillboardButton.Value.bounds.Width / 2 + ? LanguageKeys.Calendar + : LanguageKeys.Billboard; + IClickableMenu.drawHoverText(Game1.spriteBatch, _helper.SafeGetString(hoverText), Game1.dialogueFont); + } + } - private void ActivateBillboard() + private void ActivateBillboard() + { + if (Game1.activeClickableMenu is GameMenu gameMenu && + gameMenu.currentTab == 0 && + _heldItem.Value == null && + _showBillboardButton.Value.containsPoint( + (int)Utility.ModifyCoordinateForUIScale(Game1.getMouseX()), + (int)Utility.ModifyCoordinateForUIScale(Game1.getMouseY()) + )) + { + if (Game1.questOfTheDay != null && string.IsNullOrEmpty(Game1.questOfTheDay.currentObjective)) { - if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 0 - && _heldItem.Value == null - && _showBillboardButton.Value.containsPoint((int)Utility.ModifyCoordinateForUIScale(Game1.getMouseX()), (int)Utility.ModifyCoordinateForUIScale(Game1.getMouseY()))) - { - if (Game1.questOfTheDay != null && - string.IsNullOrEmpty(Game1.questOfTheDay.currentObjective)) - Game1.questOfTheDay.currentObjective = "wat?"; - - Game1.activeClickableMenu = - new Billboard(!(Utility.ModifyCoordinateForUIScale(Game1.getMouseX()) < - _showBillboardButton.Value.bounds.X + _showBillboardButton.Value.bounds.Width / 2)); - } + Game1.questOfTheDay.currentObjective = "wat?"; } - #endregion + + Game1.activeClickableMenu = new Billboard( + !(Utility.ModifyCoordinateForUIScale(Game1.getMouseX()) < + _showBillboardButton.Value.bounds.X + _showBillboardButton.Value.bounds.Width / 2) + ); + } } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowCropAndBarrelTime.cs b/UIInfoSuite2/UIElements/ShowCropAndBarrelTime.cs index 1006a473..f951198a 100644 --- a/UIInfoSuite2/UIElements/ShowCropAndBarrelTime.cs +++ b/UIInfoSuite2/UIElements/ShowCropAndBarrelTime.cs @@ -1,374 +1,465 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Buildings; -using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Objects; using StardewValley.TerrainFeatures; -using System; -using System.Collections.Generic; -using System.Text; +using UIInfoSuite2.Compatibility; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; +using Object = StardewValley.Object; namespace UIInfoSuite2.UIElements { - internal class ShowCropAndBarrelTime : IDisposable + internal class ShowCropAndBarrelTime : IDisposable + { + private readonly PerScreen _currentTile = new(); + private readonly PerScreen _currentTileBuilding = new(); + private readonly IModHelper _helper; + private readonly Dictionary _indexOfCropNames = new(); + + private readonly Dictionary _indexOfDgaCropNames = new(); + private readonly PerScreen _terrain = new(); + + public ShowCropAndBarrelTime(IModHelper helper) { - private readonly Dictionary _indexOfCropNames = new(); - private readonly PerScreen _currentTile = new(); - private readonly PerScreen _terrain = new(); - private readonly PerScreen _currentTileBuilding = new(); - private readonly IModHelper _helper; + _helper = helper; + } - private readonly Dictionary _indexOfDgaCropNames = new(); + public void Dispose() + { + ToggleOption(false); + } - public ShowCropAndBarrelTime(IModHelper helper) + public void ToggleOption(bool showCropAndBarrelTimes) + { + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showCropAndBarrelTimes) + { + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + } + } + + /// Raised after the game state is updated (≈60 times per second). + /// The event sender. + /// The event arguments. + private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + { + if (!e.IsMultipleOf(4)) + { + return; + } + + _currentTileBuilding.Value = null; + _currentTile.Value = null; + _terrain.Value = null; + + Vector2 gamepadTile = Game1.player.CurrentTool != null + ? Utility.snapToInt(Game1.player.GetToolLocation() / Game1.tileSize) + : Utility.snapToInt(Game1.player.GetGrabTile()); + Vector2 mouseTile = Game1.currentCursorTile; + + Vector2 tile = Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0 ? gamepadTile : mouseTile; + + if (Game1.currentLocation.IsBuildableLocation()) + { + _currentTileBuilding.Value = Game1.currentLocation.getBuildingAt(tile); + } + + if (Game1.currentLocation != null) + { + if (Game1.currentLocation.Objects != null && + Game1.currentLocation.Objects.TryGetValue(tile, out Object? currentObject)) { - _helper = helper; + _currentTile.Value = currentObject; } - public void ToggleOption(bool showCropAndBarrelTimes) + if (Game1.currentLocation.terrainFeatures != null && + Game1.currentLocation.terrainFeatures.TryGetValue(tile, out TerrainFeature? terrain)) { - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - - if (showCropAndBarrelTimes) - { - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - } + _terrain.Value = terrain; } - /// Raised after the game state is updated (≈60 times per second). - /// The event sender. - /// The event arguments. - private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + // Make sure that _terrain is null before overwriting it because Tea Saplings are added to terrainFeatures and not IndoorPot.bush + if (_terrain.Value == null && _currentTile.Value is IndoorPot pot) { - if (!e.IsMultipleOf(4)) - return; - - _currentTileBuilding.Value = null; - _currentTile.Value = null; - _terrain.Value = null; - - var gamepadTile = Game1.player.CurrentTool != null ? Utility.snapToInt(Game1.player.GetToolLocation() / Game1.tileSize) : Utility.snapToInt(Game1.player.GetGrabTile()); - var mouseTile = Game1.currentCursorTile; - - var tile = (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) ? gamepadTile : mouseTile; - - if (Game1.currentLocation is BuildableGameLocation buildableLocation) - _currentTileBuilding.Value = buildableLocation.getBuildingAt(tile); + if (pot.hoeDirt.Value != null) + { + _terrain.Value = pot.hoeDirt.Value; + } + + if (pot.bush.Value != null) + { + _terrain.Value = pot.bush.Value; + } + } + } + } - if (Game1.currentLocation != null) + /// + /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this + /// point (e.g. because a menu is open). + /// + /// The event sender. + /// The event arguments. + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + if (Game1.activeClickableMenu != null) + { + return; + } + + Building? currentTileBuilding = _currentTileBuilding.Value; + Object? currentTile = _currentTile.Value; + TerrainFeature? terrain = _terrain.Value; + + int overrideX = -1; + int overrideY = -1; + + // draw hover tooltip + var inputKey = 0; + // TODO1.6 <= The tooltip for Mill says: + // The Mill class is only used to preserve data from old save files. All mills were converted into plain Building instances based on the rules in Data/Buildings. + // The input and output items are now stored in Building.buildingChests with the 'Input' and 'Output' keys respectively. + // Perhaps this was written when the 'buildingChests' property was a dictionary. Now it's a list, and there's no property on Chest or ChestData + // that indicates which chest is the input and which is the output... I must be missing something. + // if (currentTileBuilding != null && currentTileBuilding is Mill millBuilding && millBuilding.input.Value != null && !millBuilding.input.Value.isEmpty()) + if (currentTileBuilding != null && + currentTileBuilding.buildingChests.Count > inputKey && + !currentTileBuilding.buildingChests[inputKey].isEmpty()) + { + var wheatCount = 0; + var beetCount = 0; + var unmilledriceCount = 0; + + foreach (Item item in currentTileBuilding.buildingChests[inputKey].Items) + { + if (item != null && !string.IsNullOrEmpty(item.Name)) + { + switch (item.Name) { - if (Game1.currentLocation.Objects != null && Game1.currentLocation.Objects.TryGetValue(tile, out var currentObject)) - _currentTile.Value = currentObject; - - if (Game1.currentLocation.terrainFeatures != null && Game1.currentLocation.terrainFeatures.TryGetValue(tile, out var terrain)) - _terrain.Value = terrain; - - // Make sure that _terrain is null before overwriting it because Tea Saplings are added to terrainFeatures and not IndoorPot.bush - if (_terrain.Value == null && _currentTile.Value is IndoorPot pot) - { - if (pot.hoeDirt.Value != null) - _terrain.Value = pot.hoeDirt.Value; - if (pot.bush.Value != null) - _terrain.Value = pot.bush.Value; - } + case "Wheat": + wheatCount += item.Stack; + break; + case "Beet": + beetCount += item.Stack; + break; + case "Unmilled Rice": + unmilledriceCount += item.Stack; + break; } + } } - public void Dispose() + var builder = new StringBuilder(); + + if (wheatCount > 0) { - ToggleOption(false); + builder.Append($"{ItemRegistry.GetData("(O)262").DisplayName}:{wheatCount}"); } - /// Raised before drawing the HUD (item toolbar, clock, etc) to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). - /// The event sender. - /// The event arguments. - private void OnRenderingHud(object sender, RenderingHudEventArgs e) + if (beetCount > 0) { - if (Game1.activeClickableMenu != null) - return; + if (wheatCount > 0) + { + builder.Append(Environment.NewLine); + } - var currentTileBuilding = _currentTileBuilding.Value; - var currentTile = _currentTile.Value; - var terrain = _terrain.Value; - - int overrideX = -1; - int overrideY = -1; + builder.Append($"{ItemRegistry.GetData("(O)284").DisplayName}:{beetCount}"); + } - // draw hover tooltip - if (currentTileBuilding != null && currentTileBuilding is Mill millBuilding && millBuilding.input.Value != null && !millBuilding.input.Value.isEmpty()) - { - int wheatCount = 0; - int beetCount = 0; + if (unmilledriceCount > 0) + { + if (beetCount > 0 || wheatCount > 0) + { + builder.Append(Environment.NewLine); + } - foreach (Item item in millBuilding.input.Value.items) - { - if (item != null && - !string.IsNullOrEmpty(item.Name)) - { - switch (item.Name) - { - case "Wheat": wheatCount = item.Stack; break; - case "Beet": beetCount = item.Stack; break; - } - } - } + builder.Append($"{ItemRegistry.GetData("(O)271").DisplayName}:{unmilledriceCount}"); + } - StringBuilder builder = new StringBuilder(); + if (builder.Length > 0) + { + if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) + { + Vector2 tilePosition = Utility.ModifyCoordinatesForUIScale( + Game1.GlobalToLocal( + new Vector2(currentTileBuilding.tileX.Value, currentTileBuilding.tileY.Value) * Game1.tileSize + ) + ); + overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); + overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); + } + + IClickableMenu.drawHoverText( + Game1.spriteBatch, + builder.ToString(), + Game1.smallFont, + overrideX: overrideX, + overrideY: overrideY + ); + } + } + else if (currentTile != null && (!currentTile.bigCraftable.Value || currentTile.MinutesUntilReady > 0)) + { + if (currentTile.bigCraftable.Value && + currentTile.MinutesUntilReady > 0 && + currentTile.heldObject.Value != null && + currentTile.Name != "Heater") + { + var hoverText = new StringBuilder(); + + hoverText.AppendLine(currentTile.heldObject.Value.DisplayName); + + if (currentTile is Cask) + { + var currentCask = currentTile as Cask; + hoverText.Append((int)(currentCask.daysToMature.Value / currentCask.agingRate.Value)) + .Append(" " + _helper.SafeGetString(LanguageKeys.DaysToMature)); + } + else + { + int timeLeft = currentTile.MinutesUntilReady; + int longTime = timeLeft / 60; + string longText = LanguageKeys.Hours; + int shortTime = timeLeft % 60; + string shortText = LanguageKeys.Minutes; + + // 1600 minutes per day if you go to bed at 2am, more if you sleep early. + if (timeLeft >= 1600) + { + // Unlike crops and casks, this is only an approximate number of days + // because of how time works while sleeping. It's close enough though. + longText = LanguageKeys.Days; + longTime = timeLeft / 1600; + + shortText = LanguageKeys.Hours; + shortTime = timeLeft % 1600; + + // Hours below 1200 are 60 minutes per hour. Overnight it's 100 minutes per hour. + // We could just divide by 60 here but then you could see strange times like + // "2 days, 25 hours". + // This is a bit of a fudge since depending on the current time of day and when the + // farmer goes to bed, the night might happen earlier or last longer, but it's just + // an approximation; regardless the processing won't finish before tomorrow. + if (shortTime <= 1200) + { + shortTime /= 60; + } + else + { + shortTime = 20 + (shortTime - 1200) / 100; + } + } - if (wheatCount > 0) - builder.Append(wheatCount + " wheat"); + if (longTime > 0) + { + hoverText.Append(longTime).Append(" ").Append(_helper.SafeGetString(longText)).Append(", "); + } - if (beetCount > 0) - { - if (wheatCount > 0) - builder.Append(Environment.NewLine); - builder.Append(beetCount + " beets"); - } + hoverText.Append(shortTime).Append(" ").Append(_helper.SafeGetString(shortText)); + } + + if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) + { + Vector2 tilePosition = Utility.ModifyCoordinatesForUIScale( + Game1.GlobalToLocal(new Vector2(currentTile.TileLocation.X, currentTile.TileLocation.Y) * Game1.tileSize) + ); + overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); + overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); + } + + IClickableMenu.drawHoverText( + Game1.spriteBatch, + hoverText.ToString(), + Game1.smallFont, + overrideX: overrideX, + overrideY: overrideY + ); + } + } + else if (terrain != null) + { + if (terrain is HoeDirt) + { + var hoeDirt = terrain as HoeDirt; + if (hoeDirt.crop != null && !hoeDirt.crop.dead.Value) + { + var num = 0; - if (builder.Length > 0) - { - if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) - { - var tilePosition = Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(new Vector2(currentTileBuilding.tileX.Value, currentTileBuilding.tileY.Value) * Game1.tileSize)); - overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); - overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); - } - - IClickableMenu.drawHoverText( - Game1.spriteBatch, - builder.ToString(), - Game1.smallFont, overrideX: overrideX, overrideY: overrideY); - } - } - else if (currentTile != null && - (!currentTile.bigCraftable.Value || - currentTile.MinutesUntilReady > 0)) + if (hoeDirt.crop.fullyGrown.Value && hoeDirt.crop.dayOfCurrentPhase.Value > 0) { - if (currentTile.bigCraftable.Value && - currentTile.MinutesUntilReady > 0 && - currentTile.heldObject.Value != null && - currentTile.Name != "Heater") - { - StringBuilder hoverText = new StringBuilder(); - - hoverText.AppendLine(currentTile.heldObject.Value.DisplayName); - - if (currentTile is Cask) - { - Cask currentCask = currentTile as Cask; - hoverText.Append((int)(currentCask.daysToMature.Value / currentCask.agingRate.Value)) - .Append(" " + _helper.SafeGetString( - LanguageKeys.DaysToMature)); - } - else - { - int timeLeft = currentTile.MinutesUntilReady; - int longTime = timeLeft / 60; - string longText = LanguageKeys.Hours; - int shortTime = timeLeft % 60; - string shortText = LanguageKeys.Minutes; - - // 1600 minutes per day if you go to bed at 2am, more if you sleep early. - if (timeLeft >= 1600) - { - // Unlike crops and casks, this is only an approximate number of days - // because of how time works while sleeping. It's close enough though. - longText = LanguageKeys.Days; - longTime = timeLeft / 1600; - - shortText = LanguageKeys.Hours; - shortTime = (timeLeft % 1600); - - // Hours below 1200 are 60 minutes per hour. Overnight it's 100 minutes per hour. - // We could just divide by 60 here but then you could see strange times like - // "2 days, 25 hours". - // This is a bit of a fudge since depending on the current time of day and when the - // farmer goes to bed, the night might happen earlier or last longer, but it's just - // an approximation; regardless the processing won't finish before tomorrow. - if (shortTime <= 1200) - shortTime /= 60; - else - shortTime = 20 + (shortTime - 1200) / 100; - } - - if (longTime > 0) - hoverText.Append(longTime).Append(" ") - .Append(_helper.SafeGetString(longText)) - .Append(", "); - - hoverText.Append(shortTime).Append(" ") - .Append(_helper.SafeGetString(shortText)); - } - - if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) - { - var tilePosition = Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(new Vector2(currentTile.TileLocation.X, currentTile.TileLocation.Y) * Game1.tileSize)); - overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); - overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); - } - - IClickableMenu.drawHoverText( - Game1.spriteBatch, - hoverText.ToString(), - Game1.smallFont, overrideX: overrideX, overrideY: overrideY); - } + num = hoeDirt.crop.dayOfCurrentPhase.Value; } - else if (terrain != null) + else { - if (terrain is HoeDirt) - { - HoeDirt hoeDirt = terrain as HoeDirt; - if (hoeDirt.crop != null && - !hoeDirt.crop.dead.Value) - { - int num = 0; - - if (hoeDirt.crop.fullyGrown.Value && - hoeDirt.crop.dayOfCurrentPhase.Value > 0) - { - num = hoeDirt.crop.dayOfCurrentPhase.Value; - } - else - { - for (int i = 0; i < hoeDirt.crop.phaseDays.Count - 1; ++i) - { - if (hoeDirt.crop.currentPhase.Value == i) - num -= hoeDirt.crop.dayOfCurrentPhase.Value; - - if (hoeDirt.crop.currentPhase.Value <= i) - num += hoeDirt.crop.phaseDays[i]; - } - } - - string? harvestName = this.GetCropHarvestName(hoeDirt.crop); - if (!String.IsNullOrEmpty(harvestName)) - { - StringBuilder hoverText = new StringBuilder(harvestName).Append(": "); - if (num > 0) - { - hoverText.Append(num).Append(" ") - .Append(_helper.SafeGetString( - LanguageKeys.Days)); - } - else - { - hoverText.Append(_helper.SafeGetString( - LanguageKeys.ReadyToHarvest)); - } - - if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) - { - var tilePosition = Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(new Vector2(terrain.currentTileLocation.X, terrain.currentTileLocation.Y) * Game1.tileSize)); - overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); - overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); - } - - IClickableMenu.drawHoverText( - Game1.spriteBatch, - hoverText.ToString(), - Game1.smallFont, overrideX: overrideX, overrideY: overrideY); - } - } - } - else if (terrain is FruitTree) + for (var i = 0; i < hoeDirt.crop.phaseDays.Count - 1; ++i) + { + if (hoeDirt.crop.currentPhase.Value == i) { - FruitTree tree = terrain as FruitTree; - var text = new StardewValley.Object(new Debris(tree.indexOfFruit.Value, Vector2.Zero, Vector2.Zero).chunkType.Value, 1).DisplayName; - if (tree.daysUntilMature.Value > 0) - { - text += Environment.NewLine + tree.daysUntilMature.Value + " " + - _helper.SafeGetString( - LanguageKeys.DaysToMature); - - } - - if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) - { - var tilePosition = Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(new Vector2(terrain.currentTileLocation.X, terrain.currentTileLocation.Y) * Game1.tileSize)); - overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); - overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); - } - - IClickableMenu.drawHoverText( - Game1.spriteBatch, - text, - Game1.smallFont, overrideX: overrideX, overrideY: overrideY); + num -= hoeDirt.crop.dayOfCurrentPhase.Value; } - else if (terrain is Bush bush) + + if (hoeDirt.crop.currentPhase.Value <= i) { - // Tea saplings (which are actually bushes) - if (bush.size.Value == Bush.greenTeaBush) - { - int teaAge = bush.getAge(); - if (teaAge < 20) - { - string text = new StardewValley.Object(251, 1).DisplayName - + $"\n{20 - teaAge} " - + _helper.SafeGetString(LanguageKeys.DaysToMature); - - if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) - { - var tilePosition = Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(new Vector2(terrain.currentTileLocation.X, terrain.currentTileLocation.Y) * Game1.tileSize)); - overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); - overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); - } - - IClickableMenu.drawHoverText( - Game1.spriteBatch, - text, - Game1.smallFont, overrideX: overrideX, overrideY: overrideY); - } - } + num += hoeDirt.crop.phaseDays[i]; } + } } - } - string? GetCropHarvestName(Crop crop) - { - if (crop.indexOfHarvest.Value > 0) + string? harvestName = GetCropHarvestName(hoeDirt.crop); + if (!string.IsNullOrEmpty(harvestName)) { - int itemId = crop.isWildSeedCrop() ? crop.whichForageCrop.Value : crop.indexOfHarvest.Value; - if (!_indexOfCropNames.TryGetValue(itemId, out string? harvestName)) { - harvestName = new StardewValley.Object(itemId, 1).DisplayName; - _indexOfCropNames.Add(itemId, harvestName); - } - return harvestName; + StringBuilder hoverText = new StringBuilder(harvestName).Append(": "); + if (num > 0) + { + hoverText.Append(num).Append(" ").Append(_helper.SafeGetString(LanguageKeys.Days)); + } + else + { + hoverText.Append(_helper.SafeGetString(LanguageKeys.ReadyToHarvest)); + } + + if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) + { + Vector2 tilePosition = Utility.ModifyCoordinatesForUIScale( + Game1.GlobalToLocal(new Vector2(terrain.Tile.X, terrain.Tile.Y) * Game1.tileSize) + ); + overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); + overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); + } + + IClickableMenu.drawHoverText( + Game1.spriteBatch, + hoverText.ToString(), + Game1.smallFont, + overrideX: overrideX, + overrideY: overrideY + ); } - else if (ModEntry.DGA.IsCustomCrop(crop, out var dgaHelper)) + } + } + else if (terrain is FruitTree tree) + { + string itemIdOfFruit = + tree.GetData().Fruit.First().ItemId; // TODO 1.6: Might be broken because of more than one item. + string? text = ItemRegistry.GetData(itemIdOfFruit).DisplayName; + if (tree.daysUntilMature.Value > 0) + { + text += Environment.NewLine + + tree.daysUntilMature.Value + + " " + + _helper.SafeGetString(LanguageKeys.DaysToMature); + } + + if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) + { + Vector2 tilePosition = + Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(terrain.Tile * Game1.tileSize)); + overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); + overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); + } + + IClickableMenu.drawHoverText( + Game1.spriteBatch, + text, + Game1.smallFont, + overrideX: overrideX, + overrideY: overrideY + ); + } + else if (terrain is Bush bush) + { + // Tea saplings (which are actually bushes) + if (bush.size.Value == Bush.greenTeaBush) + { + int teaAge = bush.getAge(); + if (teaAge < 20) { - string? cropId = null; - try - { - cropId = dgaHelper.GetFullId(crop)!; - if (!_indexOfDgaCropNames.TryGetValue(cropId, out string? harvestName)) { - var harvestCrop = dgaHelper.GetCropHarvest(crop); - if (harvestCrop == null) - return null; - - harvestName = harvestCrop.DisplayName; - _indexOfDgaCropNames.Add(cropId, harvestName); - } - return harvestName; - } - catch (Exception e) - { - ModEntry.MonitorObject.LogOnce($"An error occured while retrieving the crop harvest name for {cropId ?? "unknownCrop"}.", LogLevel.Error); - ModEntry.MonitorObject.Log(e.ToString(), LogLevel.Debug); - return null; - } + string text = new Object("(O)251", 1).DisplayName // 251 <- Tea Sapling + + + $"\n{20 - teaAge} " + + _helper.SafeGetString(LanguageKeys.DaysToMature); + + if (Game1.options.gamepadControls && Game1.timerUntilMouseFade <= 0) + { + Vector2 tilePosition = + Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(terrain.Tile * Game1.tileSize)); + overrideX = (int)(tilePosition.X + Utility.ModifyCoordinateForUIScale(32)); + overrideY = (int)(tilePosition.Y + Utility.ModifyCoordinateForUIScale(32)); + } + + IClickableMenu.drawHoverText( + Game1.spriteBatch, + text, + Game1.smallFont, + overrideX: overrideX, + overrideY: overrideY + ); } - else + } + } + } + } + + private string? GetCropHarvestName(Crop crop) + { + if (crop.indexOfHarvest.Value is not null) + { + // If you look at Crop.cs in the decompiled sources, it seems that there's a special case for spring onions - that's what the =="1" is about. + string itemId = crop.whichForageCrop.Value == "1" ? "399" : + crop.isWildSeedCrop() ? crop.whichForageCrop.Value : crop.indexOfHarvest.Value; + if (!_indexOfCropNames.TryGetValue(itemId, out string? harvestName)) + { + harvestName = new Object(itemId, 1).DisplayName; + _indexOfCropNames.Add(itemId, harvestName); + } + + return harvestName; + } + + if (ModEntry.DGA.IsCustomCrop(crop, out DynamicGameAssetsHelper? dgaHelper)) + { + string? cropId = null; + try + { + cropId = dgaHelper.GetFullId(crop)!; + if (!_indexOfDgaCropNames.TryGetValue(cropId, out string? harvestName)) + { + Object? harvestCrop = dgaHelper.GetCropHarvest(crop); + if (harvestCrop == null) { - return null; + return null; } + + harvestName = harvestCrop.DisplayName; + _indexOfDgaCropNames.Add(cropId, harvestName); + } + + return harvestName; } + catch (Exception e) + { + ModEntry.MonitorObject.LogOnce( + $"An error occured while retrieving the crop harvest name for {cropId ?? "unknownCrop"}.", + LogLevel.Error + ); + ModEntry.MonitorObject.Log(e.ToString(), LogLevel.Debug); + return null; + } + } + + return null; } + } } diff --git a/UIInfoSuite2/UIElements/ShowItemEffectRanges.cs b/UIInfoSuite2/UIElements/ShowItemEffectRanges.cs index fd3926c2..43bcd401 100644 --- a/UIInfoSuite2/UIElements/ShowItemEffectRanges.cs +++ b/UIInfoSuite2/UIElements/ShowItemEffectRanges.cs @@ -1,322 +1,370 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Threading; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Buildings; -using StardewValley.Locations; -using System; -using System.Collections.Generic; -using System.Threading; +using StardewValley.Network; +using Object = StardewValley.Object; namespace UIInfoSuite2.UIElements { - internal class ShowItemEffectRanges : IDisposable - { - #region Properties - private readonly PerScreen> _effectiveArea = new(createNewState: () => new List()); + internal class ShowItemEffectRanges : IDisposable + { + #region Properties + private readonly PerScreen> _effectiveArea = new(() => new List()); - private readonly Mutex _mutex = new(); + private readonly Mutex _mutex = new(); - private readonly IModHelper _helper; - #endregion + private readonly IModHelper _helper; + #endregion - #region Lifecycle - public ShowItemEffectRanges(IModHelper helper) - { - _helper = helper; - } + #region Lifecycle + public ShowItemEffectRanges(IModHelper helper) + { + _helper = helper; + } + + public void Dispose() + { + ToggleOption(false); + } - public void Dispose() + public void ToggleOption(bool showItemEffectRanges) + { + _helper.Events.Display.Rendered -= OnRendered; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showItemEffectRanges) + { + _helper.Events.Display.Rendered += OnRendered; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + } + } + #endregion + + + #region Event subscriptions + private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + { + if (!e.IsMultipleOf(4)) + { + return; + } + + // Ticks can happen when the player reverts to the loading screen; defend against that. + if (Game1.currentLocation is null) + { + return; + } + + if (_mutex.WaitOne()) + { + try { - ToggleOption(false); + _effectiveArea.Value.Clear(); } - - public void ToggleOption(bool showItemEffectRanges) + finally { - _helper.Events.Display.Rendered -= OnRendered; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - - if (showItemEffectRanges) - { - _helper.Events.Display.Rendered += OnRendered; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - } + _mutex.ReleaseMutex(); } - #endregion + } + if (Game1.activeClickableMenu == null && !Game1.eventUp) + { + UpdateEffectiveArea(); + } + } - #region Event subscriptions - private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + private void OnRendered(object sender, RenderedEventArgs e) + { + if (_mutex.WaitOne(0)) + { + try { - if (!e.IsMultipleOf(4)) - return; - - if (_mutex.WaitOne()) - { - try - { - _effectiveArea.Value.Clear(); - } - finally - { - _mutex.ReleaseMutex(); - } - - } - - if (Game1.activeClickableMenu == null && !Game1.eventUp) - { - UpdateEffectiveArea(); - } + foreach (Point point in _effectiveArea.Value) + { + var position = new Vector2( + point.X * Utility.ModifyCoordinateFromUIScale(Game1.tileSize), + point.Y * Utility.ModifyCoordinateFromUIScale(Game1.tileSize) + ); + Game1.spriteBatch.Draw( + Game1.mouseCursors, + Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(Utility.ModifyCoordinatesForUIScale(position))), + new Rectangle(194, 388, 16, 16), + Color.White * 0.7f, + 0.0f, + Vector2.Zero, + Utility.ModifyCoordinateForUIScale(Game1.pixelZoom), + SpriteEffects.None, + 0.01f + ); + } } - - private void OnRendered(object sender, RenderedEventArgs e) + finally { - if (_mutex.WaitOne(0)) - { - try - { - foreach (Point point in _effectiveArea.Value) - { - var position = new Vector2(point.X * Utility.ModifyCoordinateFromUIScale(Game1.tileSize), point.Y * Utility.ModifyCoordinateFromUIScale(Game1.tileSize)); - Game1.spriteBatch.Draw( - Game1.mouseCursors, - Utility.ModifyCoordinatesForUIScale(Game1.GlobalToLocal(Utility.ModifyCoordinatesForUIScale(position))), - new Rectangle(194, 388, 16, 16), - Color.White * 0.7f, - 0.0f, - Vector2.Zero, - Utility.ModifyCoordinateForUIScale(Game1.pixelZoom), - SpriteEffects.None, - 0.01f); - } - } - finally - { - _mutex.ReleaseMutex(); - } - } + _mutex.ReleaseMutex(); } - #endregion + } + } + #endregion - #region Logic - private void UpdateEffectiveArea() - { - int[][] arrayToUse; - List similarObjects; + #region Logic + private void UpdateEffectiveArea() + { + int[][] arrayToUse; + List similarObjects; - // Junimo Hut is handled differently, because it is a building - if (Game1.currentLocation is BuildableGameLocation buildableLocation) - { - Building building = buildableLocation.getBuildingAt(Game1.GetPlacementGrabTile()); - - if (building is JunimoHut) - { - arrayToUse = GetDistanceArray(ObjectsWithDistance.JunimoHut); - foreach (var nextBuilding in buildableLocation.buildings) - { - if (nextBuilding is JunimoHut nextHut) - { - AddTilesToHighlightedArea(arrayToUse, nextHut.tileX.Value + 1, nextHut.tileY.Value + 1); - } - } - } - } + // Junimo Hut is handled differently, because it is a building + Building building = Game1.currentLocation.getBuildingAt(Game1.GetPlacementGrabTile()); - // Every other item is here - if (Game1.player.CurrentItem is StardewValley.Object currentItem && currentItem.isPlaceable()) - { - string itemName = currentItem.Name; - - Vector2 currentTile = Game1.GetPlacementGrabTile(); - Game1.isCheckingNonMousePlacement = !Game1.IsPerformingMousePlacement(); - Vector2 validTile = Utility.snapToInt(Utility.GetNearbyValidPlacementPosition(Game1.player, Game1.currentLocation, currentItem, (int)currentTile.X * Game1.tileSize, (int)currentTile.Y * Game1.tileSize)) / Game1.tileSize; - Game1.isCheckingNonMousePlacement = false; - - if (itemName.IndexOf("arecrow", StringComparison.OrdinalIgnoreCase) >= 0) - { - arrayToUse = itemName.Contains("eluxe") ? - GetDistanceArray(ObjectsWithDistance.DeluxeScarecrow, false, currentItem) : - GetDistanceArray(ObjectsWithDistance.Scarecrow, false, currentItem); - AddTilesToHighlightedArea(arrayToUse, (int)validTile.X, (int)validTile.Y); - - similarObjects = GetSimilarObjectsInLocation("arecrow"); - foreach (StardewValley.Object next in similarObjects) - { - arrayToUse = next.Name.IndexOf("eluxe", StringComparison.OrdinalIgnoreCase) >= 0 ? - GetDistanceArray(ObjectsWithDistance.DeluxeScarecrow, false, next) : - GetDistanceArray(ObjectsWithDistance.Scarecrow, false, next); - AddTilesToHighlightedArea(arrayToUse, (int)next.TileLocation.X, (int)next.TileLocation.Y); - } - } - else if (itemName.IndexOf("sprinkler", StringComparison.OrdinalIgnoreCase) >= 0) - { - // Relative tile positions to the placable items locations - need to pass coordinates - AddTilesToHighlightedArea(currentItem.GetSprinklerTiles(), (int)validTile.X, (int)validTile.Y); - - similarObjects = GetSimilarObjectsInLocation("sprinkler"); - foreach (StardewValley.Object next in similarObjects) - { - // Absolute tile positions - AddTilesToHighlightedArea(next.GetSprinklerTiles()); - } - } - else if (itemName.IndexOf("bee house", StringComparison.OrdinalIgnoreCase) >= 0) - { - arrayToUse = GetDistanceArray(ObjectsWithDistance.Beehouse); - AddTilesToHighlightedArea(arrayToUse, (int)validTile.X, (int)validTile.Y); - } - } + if (building is JunimoHut) + { + arrayToUse = GetDistanceArray(ObjectsWithDistance.JunimoHut); + foreach (Building? nextBuilding in Game1.currentLocation.buildings) + { + if (nextBuilding is JunimoHut nextHut) + { + AddTilesToHighlightedArea(arrayToUse, nextHut.tileX.Value + 1, nextHut.tileY.Value + 1); + } } - - private void AddTilesToHighlightedArea(IEnumerable tiles, int xPos = 0, int yPos = 0) + } + + // Every other item is here + if (Game1.player.CurrentItem is Object currentItem && currentItem.isPlaceable()) + { + string itemName = currentItem.Name; + + Vector2 currentTile = Game1.GetPlacementGrabTile(); + Game1.isCheckingNonMousePlacement = !Game1.IsPerformingMousePlacement(); + Vector2 validTile = Utility.snapToInt( + Utility.GetNearbyValidPlacementPosition( + Game1.player, + Game1.currentLocation, + currentItem, + (int)currentTile.X * Game1.tileSize, + (int)currentTile.Y * Game1.tileSize + ) + ) / + Game1.tileSize; + Game1.isCheckingNonMousePlacement = false; + + if (itemName.IndexOf("arecrow", StringComparison.OrdinalIgnoreCase) >= 0) { - if (_mutex.WaitOne()) - { - try - { - foreach (var tile in tiles) - { - var point = tile.ToPoint(); - point.X += xPos; - point.Y += yPos; - _effectiveArea.Value.Add(point); - } - } - finally - { - _mutex.ReleaseMutex(); - } - } + arrayToUse = itemName.Contains("eluxe") + ? GetDistanceArray(ObjectsWithDistance.DeluxeScarecrow, false, currentItem) + : GetDistanceArray(ObjectsWithDistance.Scarecrow, false, currentItem); + AddTilesToHighlightedArea(arrayToUse, (int)validTile.X, (int)validTile.Y); + + similarObjects = GetSimilarObjectsInLocation("arecrow"); + foreach (Object next in similarObjects) + { + arrayToUse = next.Name.IndexOf("eluxe", StringComparison.OrdinalIgnoreCase) >= 0 + ? GetDistanceArray(ObjectsWithDistance.DeluxeScarecrow, false, next) + : GetDistanceArray(ObjectsWithDistance.Scarecrow, false, next); + AddTilesToHighlightedArea(arrayToUse, (int)next.TileLocation.X, (int)next.TileLocation.Y); + } } - - private void AddTilesToHighlightedArea(int[][] tileMap, int xPos = 0, int yPos = 0) + else if (itemName.IndexOf("sprinkler", StringComparison.OrdinalIgnoreCase) >= 0) { - int xOffset = tileMap.Length / 2; - - if (_mutex.WaitOne()) - { - try - { - for (int i = 0; i < tileMap.Length; ++i) - { - int yOffset = tileMap[i].Length / 2; - for (int j = 0; j < tileMap[i].Length; ++j) - { - if (tileMap[i][j] == 1) - _effectiveArea.Value.Add(new Point(xPos + i - xOffset, yPos + j - yOffset)); - } - } - } - finally - { - _mutex.ReleaseMutex(); - } - } + // Relative tile positions to the placable items locations - need to pass coordinates + + /* + * @NermNermNerm: + * This change is a little bit worrisome because Object.GetSprinklerTiles didn't semantically change in 1.6... + * But it did change. Somebody noodled over it and changed a variable name. + * However, its behavior got changed by something else - + * in the past it was zero-based - as Object.tileLocation used to be 0.0 for unplaced objects, + * and now it's the tile that's being hovered over. + * That new behavior might not be intended and might get rolled back. + */ + AddTilesToHighlightedArea(currentItem.GetSprinklerTiles()); + + similarObjects = GetSimilarObjectsInLocation("sprinkler"); + foreach (Object next in similarObjects) + { + // Absolute tile positions + AddTilesToHighlightedArea(next.GetSprinklerTiles()); + } } - - private List GetSimilarObjectsInLocation(string nameContains) + else if (itemName.IndexOf("bee house", StringComparison.OrdinalIgnoreCase) >= 0) { - List result = new List(); - - if (!string.IsNullOrEmpty(nameContains)) - { - nameContains = nameContains.ToLower(); - var objects = Game1.currentLocation.Objects; - - foreach (var nextThing in objects.Values) - { - if (nextThing.name.ToLower().Contains(nameContains)) - result.Add(nextThing); - } - } - return result; + arrayToUse = GetDistanceArray(ObjectsWithDistance.Beehouse); + AddTilesToHighlightedArea(arrayToUse, (int)validTile.X, (int)validTile.Y); } + } + } - #region Distance map - private enum ObjectsWithDistance + private void AddTilesToHighlightedArea(IEnumerable tiles, int xPos = 0, int yPos = 0) + { + if (_mutex.WaitOne()) + { + try { - JunimoHut, - Beehouse, - Scarecrow, - DeluxeScarecrow, - Sprinkler, - QualitySprinkler, - IridiumSprinkler, - PrismaticSprinkler + foreach (Vector2 tile in tiles) + { + var point = tile.ToPoint(); + point.X += xPos; + point.Y += yPos; + _effectiveArea.Value.Add(point); + } } - - private int[][] GetDistanceArray(ObjectsWithDistance type, bool hasPressureNozzle = false, StardewValley.Object? instance = null) + finally { - switch (type) - { - case ObjectsWithDistance.JunimoHut: - return GetCircularMask(100, maxDisplaySquareRadius: 8); - case ObjectsWithDistance.Beehouse: - return GetCircularMask(4.19, exceptionalDistance: 5, onlyClearExceptions: true); - case ObjectsWithDistance.Scarecrow: - return GetCircularMask((instance?.GetRadiusForScarecrow() ?? 9) - 0.01); - case ObjectsWithDistance.DeluxeScarecrow: - return GetCircularMask((instance?.GetRadiusForScarecrow() ?? 17) - 0.01); - case ObjectsWithDistance.Sprinkler: - return hasPressureNozzle ? GetCircularMask(100, maxDisplaySquareRadius: 1) : GetCircularMask(1); - case ObjectsWithDistance.QualitySprinkler: - return hasPressureNozzle ? GetCircularMask(100, maxDisplaySquareRadius: 2) : GetCircularMask(100, maxDisplaySquareRadius: 1); - case ObjectsWithDistance.IridiumSprinkler: - return hasPressureNozzle ? GetCircularMask(100, maxDisplaySquareRadius: 3) : GetCircularMask(100, maxDisplaySquareRadius: 2); - case ObjectsWithDistance.PrismaticSprinkler: - return GetCircularMask(3.69, exceptionalDistance: Math.Sqrt(18), onlyClearExceptions: false); - default: - return null; - } + _mutex.ReleaseMutex(); } + } + } - private static int[][] GetCircularMask(double maxDistance, double? exceptionalDistance = null, bool? onlyClearExceptions = null, int? maxDisplaySquareRadius = null) - { - int radius = Math.Max((int)Math.Ceiling(maxDistance), exceptionalDistance.HasValue ? (int)Math.Ceiling(exceptionalDistance.Value) : 0); - radius = Math.Min(radius, maxDisplaySquareRadius.HasValue ? maxDisplaySquareRadius.Value : radius); - int size = 2 * radius + 1; + private void AddTilesToHighlightedArea(int[][] tileMap, int xPos = 0, int yPos = 0) + { + int xOffset = tileMap.Length / 2; - int[][] result = new int[size][]; - for (int i = 0; i < size; i++) + if (_mutex.WaitOne()) + { + try + { + for (var i = 0; i < tileMap.Length; ++i) + { + int yOffset = tileMap[i].Length / 2; + for (var j = 0; j < tileMap[i].Length; ++j) { - result[i] = new int[size]; - for (int j = 0; j < size; j++) - { - double distance = GetDistance(i, j, radius); - int val = (IsInDistance(maxDistance, distance) - || (IsDistanceDirectionOK(i, j, radius, onlyClearExceptions) && IsExceptionalDistanceOK(exceptionalDistance, distance))) - ? 1 : 0; - result[i][j] = val; - } + if (tileMap[i][j] == 1) + { + _effectiveArea.Value.Add(new Point(xPos + i - xOffset, yPos + j - yOffset)); + } } - return result; + } } - - private static bool IsDistanceDirectionOK(int i, int j, int radius, bool? onlyClearExceptions) + finally { - return onlyClearExceptions.HasValue && onlyClearExceptions.Value ? (radius - j) == 0 || (radius - i) == 0 : true; + _mutex.ReleaseMutex(); } + } + } - private static bool IsExceptionalDistanceOK(double? exceptionalDistance, double distance) - { - return exceptionalDistance.HasValue && exceptionalDistance.Value == distance; - } + private List GetSimilarObjectsInLocation(string nameContains) + { + var result = new List(); + + if (!string.IsNullOrEmpty(nameContains)) + { + nameContains = nameContains.ToLower(); + OverlaidDictionary? objects = Game1.currentLocation.Objects; - private static bool IsInDistance(double maxDistance, double distance) + foreach (Object? nextThing in objects.Values) { - return distance <= maxDistance; + if (nextThing.name.ToLower().Contains(nameContains)) + { + result.Add(nextThing); + } } + } - private static double GetDistance(int i, int j, int radius) + return result; + } + + #region Distance map + private enum ObjectsWithDistance + { + JunimoHut, + Beehouse, + Scarecrow, + DeluxeScarecrow, + Sprinkler, + QualitySprinkler, + IridiumSprinkler, + PrismaticSprinkler + } + + private int[][] GetDistanceArray(ObjectsWithDistance type, bool hasPressureNozzle = false, Object? instance = null) + { + switch (type) + { + case ObjectsWithDistance.JunimoHut: + return GetCircularMask(100, maxDisplaySquareRadius: 8); + case ObjectsWithDistance.Beehouse: + return GetCircularMask(4.19, 5, true); + case ObjectsWithDistance.Scarecrow: + return GetCircularMask((instance?.GetRadiusForScarecrow() ?? 9) - 0.01); + case ObjectsWithDistance.DeluxeScarecrow: + return GetCircularMask((instance?.GetRadiusForScarecrow() ?? 17) - 0.01); + case ObjectsWithDistance.Sprinkler: + return hasPressureNozzle ? GetCircularMask(100, maxDisplaySquareRadius: 1) : GetCircularMask(1); + case ObjectsWithDistance.QualitySprinkler: + return hasPressureNozzle + ? GetCircularMask(100, maxDisplaySquareRadius: 2) + : GetCircularMask(100, maxDisplaySquareRadius: 1); + case ObjectsWithDistance.IridiumSprinkler: + return hasPressureNozzle + ? GetCircularMask(100, maxDisplaySquareRadius: 3) + : GetCircularMask(100, maxDisplaySquareRadius: 2); + case ObjectsWithDistance.PrismaticSprinkler: + return GetCircularMask(3.69, Math.Sqrt(18), false); + default: + return null; + } + } + + private static int[][] GetCircularMask( + double maxDistance, + double? exceptionalDistance = null, + bool? onlyClearExceptions = null, + int? maxDisplaySquareRadius = null + ) + { + int radius = Math.Max( + (int)Math.Ceiling(maxDistance), + exceptionalDistance.HasValue ? (int)Math.Ceiling(exceptionalDistance.Value) : 0 + ); + radius = Math.Min(radius, maxDisplaySquareRadius.HasValue ? maxDisplaySquareRadius.Value : radius); + int size = 2 * radius + 1; + + var result = new int[size][]; + for (var i = 0; i < size; i++) + { + result[i] = new int[size]; + for (var j = 0; j < size; j++) { - return Math.Sqrt((radius - i) * (radius - i) + (radius - j) * (radius - j)); + double distance = GetDistance(i, j, radius); + int val = IsInDistance(maxDistance, distance) || + (IsDistanceDirectionOK(i, j, radius, onlyClearExceptions) && + IsExceptionalDistanceOK(exceptionalDistance, distance)) + ? 1 + : 0; + result[i][j] = val; } - #endregion - #endregion + } + + return result; + } + + private static bool IsDistanceDirectionOK(int i, int j, int radius, bool? onlyClearExceptions) + { + return onlyClearExceptions.HasValue && onlyClearExceptions.Value ? radius - j == 0 || radius - i == 0 : true; + } + + private static bool IsExceptionalDistanceOK(double? exceptionalDistance, double distance) + { + return exceptionalDistance.HasValue && exceptionalDistance.Value == distance; + } + + private static bool IsInDistance(double maxDistance, double distance) + { + return distance <= maxDistance; + } + + private static double GetDistance(int i, int j, int radius) + { + return Math.Sqrt((radius - i) * (radius - i) + (radius - j) * (radius - j)); } + #endregion + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowItemHoverInformation.cs b/UIInfoSuite2/UIElements/ShowItemHoverInformation.cs index facd499f..1cd55de2 100644 --- a/UIInfoSuite2/UIElements/ShowItemHoverInformation.cs +++ b/UIInfoSuite2/UIElements/ShowItemHoverInformation.cs @@ -1,4 +1,7 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; @@ -6,529 +9,599 @@ using StardewValley; using StardewValley.Locations; using StardewValley.Menus; -using StardewValley.Objects; using StardewValley.Tools; -using System; -using System.Collections.Generic; -using System.Linq; +using UIInfoSuite2.Compatibility; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; +using Object = StardewValley.Object; namespace UIInfoSuite2.UIElements { - internal class ShowItemHoverInformation : IDisposable + internal class ShowItemHoverInformation : IDisposable + { + private readonly ClickableTextureComponent _bundleIcon = new( + new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), + Game1.mouseCursors, + new Rectangle(331, 374, 15, 14), + 3f + ); + + private readonly IModHelper _helper; + + private readonly PerScreen _hoverItem = new(); + private readonly ClickableTextureComponent _museumIcon; + private readonly Dictionary> _prunedRequiredBundles = new(); + + private readonly ClickableTextureComponent _shippingBottomIcon = new( + new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), + Game1.mouseCursors, + new Rectangle(526, 218, 30, 22), + 1.2f + ); + + private readonly ClickableTextureComponent _shippingTopIcon = new( + new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), + Game1.mouseCursors, + new Rectangle(134, 236, 30, 15), + 1.2f + ); + + private Dictionary _bundleNameOverrides; + private CommunityCenter _communityCenter; + private LibraryMuseum _libraryMuseum; + + public ShowItemHoverInformation(IModHelper helper) { - private readonly Dictionary>> _prunedRequiredBundles = new(); - private readonly ClickableTextureComponent _bundleIcon = - new( - new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), - Game1.mouseCursors, - new Rectangle(331, 374, 15, 14), - 3f); - private readonly ClickableTextureComponent _museumIcon; - private readonly ClickableTextureComponent _shippingBottomIcon = - new( - new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), - Game1.mouseCursors, - new Rectangle(526, 218, 30, 22), - 1.2f); - private readonly ClickableTextureComponent _shippingTopIcon = - new( - new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), - Game1.mouseCursors, - new Rectangle(134, 236, 30, 15), - 1.2f); - - private readonly PerScreen _hoverItem = new(); - private CommunityCenter _communityCenter; - private LibraryMuseum _libraryMuseum; - - private Dictionary _bundleNameOverrides; - - private readonly IModHelper _helper; - - public ShowItemHoverInformation(IModHelper helper) - { - _helper = helper; - - var gunther = Game1.getCharacterFromName("Gunther"); - if (gunther == null) { - ModEntry.MonitorObject.Log($"{this.GetType().Name}: Could not find Gunther in the game, creating a fake one for ourselves.", LogLevel.Warn); - gunther = new NPC() { - Name = "Gunther", - Age = 0, - Sprite = new AnimatedSprite("Characters\\Gunther"), - }; - } + _helper = helper; + + NPC? gunther = Game1.getCharacterFromName("Gunther"); + if (gunther == null) + { + ModEntry.MonitorObject.Log( + $"{GetType().Name}: Could not find Gunther in the game, creating a fake one for ourselves.", + LogLevel.Warn + ); + gunther = new NPC { Name = "Gunther", Age = 0, Sprite = new AnimatedSprite("Characters\\Gunther") }; + } + + _museumIcon = new ClickableTextureComponent( + new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), + gunther.Sprite.Texture, + gunther.GetHeadShot(), + Game1.pixelZoom + ); + } - _museumIcon = new( - new Rectangle(0, 0, Game1.tileSize, Game1.tileSize), - gunther.Sprite.Texture, - gunther.GetHeadShot(), - Game1.pixelZoom); - } + public void Dispose() + { + ToggleOption(false); + } - public void ToggleOption(bool showItemHoverInformation) + public void ToggleOption(bool showItemHoverInformation) + { + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.Player.InventoryChanged -= OnInventoryChanged; + _helper.Events.Display.Rendered -= OnRendered; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + _helper.Events.Display.Rendering -= OnRendering; + + if (showItemHoverInformation) + { + _communityCenter = Game1.getLocationFromName("CommunityCenter") as CommunityCenter; + + _libraryMuseum = Game1.getLocationFromName("ArchaeologyHouse") as LibraryMuseum; + + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.Player.InventoryChanged += OnInventoryChanged; + _helper.Events.Display.Rendered += OnRendered; + _helper.Events.Display.RenderedHud += OnRenderedHud; + _helper.Events.Display.Rendering += OnRendering; + } + } + + // [EventPriority(EventPriority.Low)] + private void OnDayStarted(object sender, DayStartedEventArgs e) + { + // NB The Custom Community Center mod is only ready at this point + if (_helper.GameContent.CurrentLocaleConstant == LocalizedContentManager.LanguageCode.en && + _helper.ModRegistry.IsLoaded("blueberry.CustomCommunityCentre")) + { + _bundleNameOverrides = GetEnglishNamesForCustomCommunityCenterBundles(); + } + else + { + _bundleNameOverrides = null; + } + + PopulateRequiredBundles(); + } + + /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. + /// The event sender. + /// The event arguments. + private void OnRendering(object sender, EventArgs e) + { + _hoverItem.Value = Tools.GetHoveredItem(); + } + + /// + /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the + /// screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch + /// at this point will appear over the HUD. + /// + /// The event sender. + /// The event arguments. + private void OnRenderedHud(object sender, EventArgs e) + { + if (Game1.activeClickableMenu == null) + { + DrawAdvancedTooltip(); + } + } + + /// + /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered + /// to the screen. + /// + /// The event sender. + /// The event arguments. + private void OnRendered(object sender, EventArgs e) + { + if (Game1.activeClickableMenu != null) + { + DrawAdvancedTooltip(); + } + } + + /// + /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for + /// the current player. + /// + /// The event sender. + /// The event arguments. + private void OnInventoryChanged(object sender, InventoryChangedEventArgs e) + { + if (e.IsLocalPlayer) + { + PopulateRequiredBundles(); + } + } + + /// Retrieve English bundle names for Custom Community Center bundles as the bundle id is not the bundle's English name. + /// For example, the bundle id is "Custom_blueberry_Kitchen_Area0_Bundle0" but the name is "Baker's" + private Dictionary GetEnglishNamesForCustomCommunityCenterBundles() + { + try + { + Dictionary englishNames = new(); + var bundleNamesAsset = _helper.GameContent.Load>(@"Strings\BundleNames"); + foreach (KeyValuePair namePair in bundleNamesAsset) { - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.Player.InventoryChanged -= OnInventoryChanged; - _helper.Events.Display.Rendered -= OnRendered; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - _helper.Events.Display.Rendering -= OnRendering; + if (namePair.Key != namePair.Value) + { + englishNames[namePair.Key] = namePair.Value; + } + } + + return englishNames; + } + catch (Exception e) + { + ModEntry.MonitorObject.LogOnce( + "Failed to retrieve English names for Custom Community Center bundles. Custom bundle names may be displayed incorrectly.", + LogLevel.Warn + ); + ModEntry.MonitorObject.Log(e.ToString()); + return null; + } + } - if (showItemHoverInformation) + private void PopulateRequiredBundles() + { + _prunedRequiredBundles.Clear(); + if (!Game1.player.mailReceived.Contains("JojaMember")) + { + foreach (KeyValuePair bundle in Game1.netWorldState.Value.BundleData) + { + string[] bundleRoomInfo = bundle.Key.Split('/'); + string bundleRoom = bundleRoomInfo[0]; + int areaNum = CommunityCenter.getAreaNumberFromName(bundleRoom); + + if (_communityCenter.shouldNoteAppearInArea(areaNum)) + { + int bundleNumber = bundleRoomInfo[1].SafeParseInt32(); + string[] bundleInfo = bundle.Value.Split('/'); + string bundleName = + _helper.GameContent.CurrentLocaleConstant == LocalizedContentManager.LanguageCode.en || + int.TryParse(bundleInfo[^1], out _) + ? bundleInfo[0] + : bundleInfo[^1]; + string[] bundleValues = bundleInfo[2].Split(' '); + List source = new(); + + for (var i = 0; i < bundleValues.Length; i += 3) { - _communityCenter = Game1.getLocationFromName("CommunityCenter") as CommunityCenter; + string bundleValue = bundleValues[i]; + int quality = bundleValues[i + 2].SafeParseInt32(); + if (bundleValue != "-1" && !_communityCenter.bundles[bundleNumber][i / 3]) + { + // 1.6ism - we're presuming that all bundles will always be Objects + source.Add(new ItemAndQuality("(O)" + bundleValue, quality)); + } + } - _libraryMuseum = Game1.getLocationFromName("ArchaeologyHouse") as LibraryMuseum; + if (source.Count > 0) + { + if (_bundleNameOverrides != null && _bundleNameOverrides.TryGetValue(bundleName, out string value)) + { + bundleName = value; + } - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.Player.InventoryChanged += OnInventoryChanged; - _helper.Events.Display.Rendered += OnRendered; - _helper.Events.Display.RenderedHud += OnRenderedHud; - _helper.Events.Display.Rendering += OnRendering; + _prunedRequiredBundles.Add(bundleName, source); } + } } + } + } - public void Dispose() - { - ToggleOption(false); - } + //private Item _lastHoverItem; + //private int lastStackSize; + //private int lastItemPrice; + //private int lastStackPrice; + //private int lastCropPrice; + //private int lastTruePrice; + //private int lastWindowWidth; + //private string lastRequiredBundleName; - // [EventPriority(EventPriority.Low)] - private void OnDayStarted(object sender, DayStartedEventArgs e) + private void DrawAdvancedTooltip() + { + if (_hoverItem.Value != null && + !(_hoverItem.Value is MeleeWeapon weapon && weapon.isScythe()) && + !(_hoverItem.Value is FishingRod)) + { + var hoveredObject = _hoverItem.Value as Object; + + int itemPrice = Tools.GetSellToStorePrice(_hoverItem.Value); + + var stackPrice = 0; + if (itemPrice > 0 && _hoverItem.Value.Stack > 1) { - // NB The Custom Community Center mod is only ready at this point - if (_helper.GameContent.CurrentLocaleConstant == LocalizedContentManager.LanguageCode.en && _helper.ModRegistry.IsLoaded("blueberry.CustomCommunityCentre")) - _bundleNameOverrides = GetEnglishNamesForCustomCommunityCenterBundles(); - else - _bundleNameOverrides = null; - PopulateRequiredBundles(); + stackPrice = itemPrice * _hoverItem.Value.Stack; } - /// Raised before the game draws anything to the screen in a draw tick, as soon as the sprite batch is opened. - /// The event sender. - /// The event arguments. - private void OnRendering(object sender, EventArgs e) + int cropPrice = Tools.GetHarvestPrice(_hoverItem.Value); + + bool notDonatedYet = _libraryMuseum.isItemSuitableForDonation(_hoverItem.Value); + + + bool notShippedYet = hoveredObject != null && + hoveredObject.countsForShippedCollection() && + !Game1.player.basicShipped.ContainsKey(hoveredObject.ItemId) && + hoveredObject.Type != "Fish"; + if (notShippedYet && + hoveredObject != null && + ModEntry.DGA.IsCustomObject(hoveredObject, out DynamicGameAssetsHelper? dgaHelper)) { - _hoverItem.Value = Tools.GetHoveredItem(); + // NB For DGA items, Game1.player.basicShipped.ContainsKey(hoveredObject.ParentSheetIndex) will always be false + // and Object.countsForShippedCollection bypasses the type and category checks + try + { + // For shipping, DGA patches: + // - CollectionsPage() + // - ShippingMenu.parseItems() + // - StardewValley.Object.countsForShippedCollection() + // - StardewValley.Object.isIndexOkForBasicShippedCategory() + // But it doesn't patch Utility.getFarmerItemsShippedPercent() which determines if the requirements for the achievement are met. + // This means that DGA items do not (yet) count for the "Full Shipment" achievement even though they appear in the collections page. + + // Nonetheless, show the icon if that item is still hidden in the collections page. + string dgaId = dgaHelper.GetDgaObjectFakeId(hoveredObject); + bool inCollectionsPage = Object.isPotentialBasicShipped(dgaId, hoveredObject.Category, hoveredObject.Type); + + notShippedYet = inCollectionsPage && !Game1.player.basicShipped.ContainsKey(dgaId); + } + catch (Exception e) + { + ModEntry.MonitorObject.LogOnce( + $"An error occured while checking if the DGA item {hoveredObject.Name} has been shipped.", + LogLevel.Error + ); + ModEntry.MonitorObject.Log(e.ToString(), LogLevel.Debug); + } } - /// Raised after drawing the HUD (item toolbar, clock, etc) to the sprite batch, but before it's rendered to the screen. The vanilla HUD may be hidden at this point (e.g. because a menu is open). Content drawn to the sprite batch at this point will appear over the HUD. - /// The event sender. - /// The event arguments. - private void OnRenderedHud(object sender, EventArgs e) + string? requiredBundleName = null; + if (hoveredObject != null) { - if (Game1.activeClickableMenu == null) + foreach (KeyValuePair> requiredBundle in _prunedRequiredBundles) + { + if (requiredBundle.Value.Any( + itemQuality => itemQuality.qualifiedItemId == hoveredObject.QualifiedItemId && + hoveredObject.Quality >= itemQuality.quality + )) { - DrawAdvancedTooltip(); + requiredBundleName = requiredBundle.Key; + break; } + } } - /// Raised after the game draws to the sprite patch in a draw tick, just before the final sprite batch is rendered to the screen. - /// The event sender. - /// The event arguments. - private void OnRendered(object sender, EventArgs e) + var drawPositionOffset = new Vector2(); + int windowWidth, windowHeight; + + var bundleHeaderWidth = 0; + if (!string.IsNullOrEmpty(requiredBundleName)) { - if (Game1.activeClickableMenu != null) - { - DrawAdvancedTooltip(); - } + // bundleHeaderWidth = ((bundleIcon.Width * 3 = 45) - 7 = 38) + 3 + bundleTextSize.X + 3 + ((shippingBin.Width * 1.2 = 36) - 12 = 24) + bundleHeaderWidth = 68 + (int)Game1.dialogueFont.MeasureString(requiredBundleName).X; } - /// Raised after items are added or removed to a player's inventory. NOTE: this event is currently only raised for the current player. - /// The event sender. - /// The event arguments. - private void OnInventoryChanged(object sender, InventoryChangedEventArgs e) + var itemTextWidth = (int)Game1.smallFont.MeasureString(itemPrice.ToString()).X; + var stackTextWidth = (int)Game1.smallFont.MeasureString(stackPrice.ToString()).X; + var cropTextWidth = (int)Game1.smallFont.MeasureString(cropPrice.ToString()).X; + var minTextWidth = (int)Game1.smallFont.MeasureString("000").X; + // largestTextWidth = 12 + 4 + (icon.Width = 32) + 4 + max(textSize.X) + 8 + 16 + int largestTextWidth = + 76 + Math.Max(minTextWidth, Math.Max(stackTextWidth, Math.Max(itemTextWidth, cropTextWidth))); + windowWidth = Math.Max(bundleHeaderWidth, largestTextWidth); + + windowHeight = 20 + 16; + if (itemPrice > 0) { - if (e.IsLocalPlayer) - this.PopulateRequiredBundles(); + windowHeight += 40; } - /// Retrieve English bundle names for Custom Community Center bundles as the bundle id is not the bundle's English name. - /// For example, the bundle id is "Custom_blueberry_Kitchen_Area0_Bundle0" but the name is "Baker's" - private Dictionary GetEnglishNamesForCustomCommunityCenterBundles() + if (stackPrice > 0) { - try - { - Dictionary englishNames = new(); - var bundleNamesAsset = _helper.GameContent.Load>(@"Strings\BundleNames"); - foreach (var namePair in bundleNamesAsset) - { - if (namePair.Key != namePair.Value) - englishNames[namePair.Key] = namePair.Value; - } - return englishNames; - } - catch (Exception e) - { - ModEntry.MonitorObject.LogOnce("Failed to retrieve English names for Custom Community Center bundles. Custom bundle names may be displayed incorrectly.", LogLevel.Warn); - ModEntry.MonitorObject.Log(e.ToString()); - return null; - } + windowHeight += 40; } - private void PopulateRequiredBundles() + if (cropPrice > 0) { - _prunedRequiredBundles.Clear(); - if (!Game1.player.mailReceived.Contains("JojaMember")) - { + windowHeight += 40; + } - foreach (var bundle in Game1.netWorldState.Value.BundleData) - { - string[] bundleRoomInfo = bundle.Key.Split('/'); - string bundleRoom = bundleRoomInfo[0]; - int areaNum = CommunityCenter.getAreaNumberFromName(bundleRoom); - - if (_communityCenter.shouldNoteAppearInArea(areaNum)) - { - int bundleNumber = bundleRoomInfo[1].SafeParseInt32(); - string[] bundleInfo = bundle.Value.Split('/'); - string bundleName = _helper.GameContent.CurrentLocaleConstant == LocalizedContentManager.LanguageCode.en || int.TryParse(bundleInfo[^1], out _) - ? bundleInfo[0] - : bundleInfo[^1]; - string[] bundleValues = bundleInfo[2].Split(' '); - List> source = new List>(); - - for (int i = 0; i < bundleValues.Length; i += 3) - { - int bundleValue = bundleValues[i].SafeParseInt32(); - int quality = bundleValues[i + 2].SafeParseInt32(); - if (bundleValue != -1 && - !_communityCenter.bundles[bundleNumber][i / 3]) - { - source.Add(new KeyValuePair(bundleValue, quality)); - } - } - - if (source.Count > 0) - { - if (_bundleNameOverrides != null && _bundleNameOverrides.TryGetValue(bundleName, out string value)) - bundleName = value; - _prunedRequiredBundles.Add(bundleName, source); - } - } - } - } + if (!string.IsNullOrEmpty(requiredBundleName)) + { + windowHeight += 4; + drawPositionOffset.Y += 4; } - //private Item _lastHoverItem; - //private int lastStackSize; - //private int lastItemPrice; - //private int lastStackPrice; - //private int lastCropPrice; - //private int lastTruePrice; - //private int lastWindowWidth; - //private string lastRequiredBundleName; + // Minimal window dimensions + windowHeight = Math.Max(windowHeight, 40); + windowWidth = Math.Max(windowWidth, 40); + + int windowY = Game1.getMouseY() + 20; + int windowX = Game1.getMouseX() - 25 - windowWidth; - private void DrawAdvancedTooltip() + // Adjust the tooltip's position when it overflows + Rectangle safeArea = Utility.getSafeArea(); + + if (windowY + windowHeight > safeArea.Bottom) { + windowY = safeArea.Bottom - windowHeight; + } - if (_hoverItem.Value != null - && !(_hoverItem.Value is StardewValley.Tools.MeleeWeapon weapon && weapon.isScythe()) - && !(_hoverItem.Value is StardewValley.Tools.FishingRod)) - { - var hoveredObject = _hoverItem.Value as StardewValley.Object; - - int itemPrice = Tools.GetSellToStorePrice(_hoverItem.Value); - - int stackPrice = 0; - if (itemPrice > 0 && _hoverItem.Value.Stack > 1) - stackPrice = itemPrice * _hoverItem.Value.Stack; - - int cropPrice = Tools.GetHarvestPrice(_hoverItem.Value); - - bool notDonatedYet = _libraryMuseum.isItemSuitableForDonation(_hoverItem.Value); - - - bool notShippedYet = (hoveredObject != null - && hoveredObject.countsForShippedCollection() - && !Game1.player.basicShipped.ContainsKey(hoveredObject.ParentSheetIndex) - && hoveredObject.Type != "Fish"); - if (notShippedYet && hoveredObject != null && ModEntry.DGA.IsCustomObject(hoveredObject, out var dgaHelper)) - { - // NB For DGA items, Game1.player.basicShipped.ContainsKey(hoveredObject.ParentSheetIndex) will always be false - // and Object.countsForShippedCollection bypasses the type and category checks - try - { - // For shipping, DGA patches: - // - CollectionsPage() - // - ShippingMenu.parseItems() - // - StardewValley.Object.countsForShippedCollection() - // - StardewValley.Object.isIndexOkForBasicShippedCategory() - // But it doesn't patch Utility.getFarmerItemsShippedPercent() which determines if the requirements for the achievement are met. - // This means that DGA items do not (yet) count for the "Full Shipment" achievement even though they appear in the collections page. - - // Nonetheless, show the icon if that item is still hidden in the collections page. - int dgaId = dgaHelper.GetDgaObjectFakeId(hoveredObject); - string t = hoveredObject.Type; - bool inCollectionsPage = !(t.Contains("Arch") || t.Contains("Fish") || t.Contains("Mineral") || t.Contains("Cooking")) - && StardewValley.Object.isPotentialBasicShippedCategory(dgaId, hoveredObject.Category.ToString()); - - notShippedYet = inCollectionsPage && !Game1.player.basicShipped.ContainsKey(dgaId); - } - catch (Exception e) - { - ModEntry.MonitorObject.LogOnce($"An error occured while checking if the DGA item {hoveredObject.Name} has been shipped.", LogLevel.Error); - ModEntry.MonitorObject.Log(e.ToString(), LogLevel.Debug); - } - } - - string? requiredBundleName = null; - // Bundle items must be "small" objects. This avoids marking other kinds of objects as needed, such as Chest (id 130), Recycling Machine (id 20), etc... - if (hoveredObject != null && !hoveredObject.bigCraftable.Value && hoveredObject is not Furniture) - { - foreach (var requiredBundle in _prunedRequiredBundles) - { - if (requiredBundle.Value.Any(itemQuality => itemQuality.Key == hoveredObject.ParentSheetIndex && hoveredObject.Quality >= itemQuality.Value)) - { - requiredBundleName = requiredBundle.Key; - break; - } - } - } - - var drawPositionOffset = new Vector2(); - int windowWidth, windowHeight; - - int bundleHeaderWidth = 0; - if (!string.IsNullOrEmpty(requiredBundleName)) - { - // bundleHeaderWidth = ((bundleIcon.Width * 3 = 45) - 7 = 38) + 3 + bundleTextSize.X + 3 + ((shippingBin.Width * 1.2 = 36) - 12 = 24) - bundleHeaderWidth = 68 + (int)Game1.dialogueFont.MeasureString(requiredBundleName).X; - } - int itemTextWidth = (int)Game1.smallFont.MeasureString(itemPrice.ToString()).X; - int stackTextWidth = (int)Game1.smallFont.MeasureString(stackPrice.ToString()).X; - int cropTextWidth = (int)Game1.smallFont.MeasureString(cropPrice.ToString()).X; - int minTextWidth = (int)Game1.smallFont.MeasureString("000").X; - // largestTextWidth = 12 + 4 + (icon.Width = 32) + 4 + max(textSize.X) + 8 + 16 - int largestTextWidth = 76 + Math.Max(minTextWidth, Math.Max(stackTextWidth, Math.Max(itemTextWidth, cropTextWidth))); - windowWidth = Math.Max(bundleHeaderWidth, largestTextWidth); - - windowHeight = 20 + 16; - if (itemPrice > 0) - windowHeight += 40; - if (stackPrice > 0) - windowHeight += 40; - if (cropPrice > 0) - windowHeight += 40; - if (!string.IsNullOrEmpty(requiredBundleName)) - { - windowHeight += 4; - drawPositionOffset.Y += 4; - } - - // Minimal window dimensions - windowHeight = Math.Max(windowHeight, 40); - windowWidth = Math.Max(windowWidth, 40); - - int windowY = Game1.getMouseY() + 20; - int windowX = Game1.getMouseX() - 25 - windowWidth; - - // Adjust the tooltip's position when it overflows - var safeArea = Utility.getSafeArea(); - - if (windowY + windowHeight > safeArea.Bottom) - windowY = safeArea.Bottom - windowHeight; - - if (Game1.getMouseX() + 300 > safeArea.Right) - windowX = safeArea.Right - 350 - windowWidth; - else if (windowX < safeArea.Left) - windowX = Game1.getMouseX() + 350; - - Vector2 windowPos = new Vector2(windowX, windowY); - Vector2 drawPosition = windowPos + new Vector2(16, 20) + drawPositionOffset; - - // Icons are drawn in 32x40 cells. The small font has a cap height of 18 and an offset of (2, 6) - int rowHeight = 40; - Vector2 iconCenterOffset = new Vector2(16, 20); - Vector2 textOffset = new Vector2(32 + 4, (rowHeight - 18) / 2 - 6); - - if (itemPrice > 0 || stackPrice > 0 || cropPrice > 0 || !String.IsNullOrEmpty(requiredBundleName) || notDonatedYet || notShippedYet) - { - IClickableMenu.drawTextureBox( - Game1.spriteBatch, - Game1.menuTexture, - new Rectangle(0, 256, 60, 60), - (int)windowPos.X, - (int)windowPos.Y, - windowWidth, - windowHeight, - Color.White); - } - - if (itemPrice > 0) - { - Game1.spriteBatch.Draw( - Game1.debrisSpriteSheet, - drawPosition + iconCenterOffset, - Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), - Color.White, - 0, - new Vector2(8, 8), - Game1.pixelZoom, - SpriteEffects.None, - 0.95f); - - this.DrawSmallTextWithShadow(Game1.spriteBatch, itemPrice.ToString(), drawPosition + textOffset); - - drawPosition.Y += rowHeight; - } - - if (stackPrice > 0) - { - Vector2 overlapOffset = new Vector2(0, 10); - Game1.spriteBatch.Draw( - Game1.debrisSpriteSheet, - drawPosition + iconCenterOffset - overlapOffset / 2, - Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), - Color.White, - 0, - new Vector2(8, 8), - Game1.pixelZoom, - SpriteEffects.None, - 0.95f); - Game1.spriteBatch.Draw( - Game1.debrisSpriteSheet, - drawPosition + iconCenterOffset + overlapOffset / 2, - Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), - Color.White, - 0, - new Vector2(8, 8), - Game1.pixelZoom, - SpriteEffects.None, - 0.95f); - - this.DrawSmallTextWithShadow(Game1.spriteBatch, stackPrice.ToString(), drawPosition + textOffset); - - drawPosition.Y += rowHeight; - } - - if (cropPrice > 0) - { - Game1.spriteBatch.Draw( - Game1.mouseCursors, - drawPosition + iconCenterOffset, - new Rectangle(60, 428, 10, 10), - Color.White, - 0.0f, - new Vector2(5, 5), - Game1.pixelZoom * 0.75f, - SpriteEffects.None, - 0.85f); - - this.DrawSmallTextWithShadow(Game1.spriteBatch, cropPrice.ToString(), drawPosition + textOffset); - } - - if (notDonatedYet) - { - Game1.spriteBatch.Draw( - _museumIcon.texture, - windowPos + new Vector2(2, windowHeight + 8), - _museumIcon.sourceRect, - Color.White, - 0f, - new Vector2(_museumIcon.sourceRect.Width / 2, _museumIcon.sourceRect.Height), - 2, - SpriteEffects.None, - 0.86f); - } - - if (!string.IsNullOrEmpty(requiredBundleName)) - { - // Draws a 30x42 bundle icon offset by (-7, -13) from the top-left corner of the window - // and the 36px high banner with the bundle name - this.DrawBundleBanner(requiredBundleName, windowPos + new Vector2(-7, -13), windowWidth); - } - - if (notShippedYet) - { - // Draws a 36x28 shipping bin offset by (-24, -6) from the top-right corner of the window - var shippingBinDims = new Vector2(30, 24); - this.DrawShippingBin(Game1.spriteBatch, windowPos + new Vector2(windowWidth - 6, 8), shippingBinDims / 2); - } - - //memorize the result to save processing time when calling again with same values - //_lastHoverItem = (_lastHoverItem != _hoverItem.Value) ? _hoverItem.Value : _lastHoverItem; - //lastItemPrice = (itemPrice != lastItemPrice) ? itemPrice : lastItemPrice; - //lastCropPrice = (lastCropPrice != cropPrice) ? cropPrice : lastCropPrice; - //lastStackPrice = (lastStackPrice != stackPrice) ? stackPrice : lastStackPrice; - //lastTruePrice = (lastTruePrice != truePrice) ? truePrice : lastTruePrice; - //lastWindowWidth = (lastWindowWidth != windowWidth) ? windowWidth : lastWindowWidth; - //lastRequiredBundleName = (lastRequiredBundleName != requiredBundleName) ? requiredBundleName : lastRequiredBundleName; - //lastStackSize = (_hoverItem.Value != null && lastStackSize != _hoverItem.Value.Stack) ? _hoverItem.Value.Stack : lastStackSize; - } + if (Game1.getMouseX() + 300 > safeArea.Right) + { + windowX = safeArea.Right - 350 - windowWidth; } + else if (windowX < safeArea.Left) + { + windowX = Game1.getMouseX() + 350; + } + + var windowPos = new Vector2(windowX, windowY); + Vector2 drawPosition = windowPos + new Vector2(16, 20) + drawPositionOffset; + + // Icons are drawn in 32x40 cells. The small font has a cap height of 18 and an offset of (2, 6) + var rowHeight = 40; + var iconCenterOffset = new Vector2(16, 20); + var textOffset = new Vector2(32 + 4, (rowHeight - 18) / 2 - 6); - private void DrawSmallTextWithShadow(SpriteBatch b, string text, Vector2 position) + if (itemPrice > 0 || + stackPrice > 0 || + cropPrice > 0 || + !string.IsNullOrEmpty(requiredBundleName) || + notDonatedYet || + notShippedYet) { - b.DrawString(Game1.smallFont, text, position + new Vector2(2, 2), Game1.textShadowColor); - b.DrawString(Game1.smallFont, text, position, Game1.textColor); + IClickableMenu.drawTextureBox( + Game1.spriteBatch, + Game1.menuTexture, + new Rectangle(0, 256, 60, 60), + (int)windowPos.X, + (int)windowPos.Y, + windowWidth, + windowHeight, + Color.White + ); } - private void DrawBundleBanner(string bundleName, Vector2 position, int windowWidth) + if (itemPrice > 0) { - // NB The dialogue font has a cap height of 30 and an offset of (3, 6) - - int bundleBannerX = (int)position.X; - int bundleBannerY = (int)position.Y + 3; - int cellCount = 36; - int solidCells = 6; - int cellWidth = windowWidth / cellCount; - for (int cell = 0; cell < cellCount; ++cell) - { - float fadeAmount = 0.92f - (cell < solidCells ? 0 : 1.0f * (cell-solidCells)/(cellCount-solidCells)); - Game1.spriteBatch.Draw( - Game1.staminaRect, - new Rectangle(bundleBannerX + cell * cellWidth, bundleBannerY, cellWidth, 36), - Color.Crimson * fadeAmount); - } + Game1.spriteBatch.Draw( + Game1.debrisSpriteSheet, + drawPosition + iconCenterOffset, + Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), + Color.White, + 0, + new Vector2(8, 8), + Game1.pixelZoom, + SpriteEffects.None, + 0.95f + ); + + DrawSmallTextWithShadow(Game1.spriteBatch, itemPrice.ToString(), drawPosition + textOffset); + + drawPosition.Y += rowHeight; + } + + if (stackPrice > 0) + { + var overlapOffset = new Vector2(0, 10); + Game1.spriteBatch.Draw( + Game1.debrisSpriteSheet, + drawPosition + iconCenterOffset - overlapOffset / 2, + Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), + Color.White, + 0, + new Vector2(8, 8), + Game1.pixelZoom, + SpriteEffects.None, + 0.95f + ); + Game1.spriteBatch.Draw( + Game1.debrisSpriteSheet, + drawPosition + iconCenterOffset + overlapOffset / 2, + Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 8, 16, 16), + Color.White, + 0, + new Vector2(8, 8), + Game1.pixelZoom, + SpriteEffects.None, + 0.95f + ); + + DrawSmallTextWithShadow(Game1.spriteBatch, stackPrice.ToString(), drawPosition + textOffset); + + drawPosition.Y += rowHeight; + } - Game1.spriteBatch.Draw( - Game1.mouseCursors, - position, - _bundleIcon.sourceRect, - Color.White, - 0f, - Vector2.Zero, - _bundleIcon.scale, - SpriteEffects.None, - 0.86f); - - Game1.spriteBatch.DrawString( - Game1.dialogueFont, - bundleName, - position + new Vector2(_bundleIcon.sourceRect.Width * _bundleIcon.scale + 3, 0), - Color.White); + if (cropPrice > 0) + { + Game1.spriteBatch.Draw( + Game1.mouseCursors, + drawPosition + iconCenterOffset, + new Rectangle(60, 428, 10, 10), + Color.White, + 0.0f, + new Vector2(5, 5), + Game1.pixelZoom * 0.75f, + SpriteEffects.None, + 0.85f + ); + + DrawSmallTextWithShadow(Game1.spriteBatch, cropPrice.ToString(), drawPosition + textOffset); + } + + if (notDonatedYet) + { + Game1.spriteBatch.Draw( + _museumIcon.texture, + windowPos + new Vector2(2, windowHeight + 8), + _museumIcon.sourceRect, + Color.White, + 0f, + new Vector2(_museumIcon.sourceRect.Width / 2, _museumIcon.sourceRect.Height), + 2, + SpriteEffects.None, + 0.86f + ); + } + + if (!string.IsNullOrEmpty(requiredBundleName)) + { + // Draws a 30x42 bundle icon offset by (-7, -13) from the top-left corner of the window + // and the 36px high banner with the bundle name + DrawBundleBanner(requiredBundleName, windowPos + new Vector2(-7, -13), windowWidth); } - private void DrawShippingBin(SpriteBatch b, Vector2 position, Vector2 origin) + if (notShippedYet) { - var shippingBinOffset = new Vector2(0, 2); - // var shippingBinLidOffset = Vector2.Zero; - - // NB This is not the texture used to draw the shipping bin on the farm map. - // The one for the farm is located in "Buildings\Shipping Bin". - Game1.spriteBatch.Draw( - _shippingBottomIcon.texture, - position, - _shippingBottomIcon.sourceRect, - Color.White, - 0f, - origin - shippingBinOffset, - _shippingBottomIcon.scale, - SpriteEffects.None, - 0.86f); - Game1.spriteBatch.Draw( - _shippingTopIcon.texture, - position, - _shippingTopIcon.sourceRect, - Color.White, - 0f, - origin, - _shippingTopIcon.scale, - SpriteEffects.None, - 0.86f); + // Draws a 36x28 shipping bin offset by (-24, -6) from the top-right corner of the window + var shippingBinDims = new Vector2(30, 24); + DrawShippingBin(Game1.spriteBatch, windowPos + new Vector2(windowWidth - 6, 8), shippingBinDims / 2); } + + //memorize the result to save processing time when calling again with same values + //_lastHoverItem = (_lastHoverItem != _hoverItem.Value) ? _hoverItem.Value : _lastHoverItem; + //lastItemPrice = (itemPrice != lastItemPrice) ? itemPrice : lastItemPrice; + //lastCropPrice = (lastCropPrice != cropPrice) ? cropPrice : lastCropPrice; + //lastStackPrice = (lastStackPrice != stackPrice) ? stackPrice : lastStackPrice; + //lastTruePrice = (lastTruePrice != truePrice) ? truePrice : lastTruePrice; + //lastWindowWidth = (lastWindowWidth != windowWidth) ? windowWidth : lastWindowWidth; + //lastRequiredBundleName = (lastRequiredBundleName != requiredBundleName) ? requiredBundleName : lastRequiredBundleName; + //lastStackSize = (_hoverItem.Value != null && lastStackSize != _hoverItem.Value.Stack) ? _hoverItem.Value.Stack : lastStackSize; + } + } + + private void DrawSmallTextWithShadow(SpriteBatch b, string text, Vector2 position) + { + b.DrawString(Game1.smallFont, text, position + new Vector2(2, 2), Game1.textShadowColor); + b.DrawString(Game1.smallFont, text, position, Game1.textColor); } + + private void DrawBundleBanner(string bundleName, Vector2 position, int windowWidth) + { + // NB The dialogue font has a cap height of 30 and an offset of (3, 6) + + var bundleBannerX = (int)position.X; + int bundleBannerY = (int)position.Y + 3; + var cellCount = 36; + var solidCells = 6; + int cellWidth = windowWidth / cellCount; + for (var cell = 0; cell < cellCount; ++cell) + { + float fadeAmount = 0.92f - (cell < solidCells ? 0 : 1.0f * (cell - solidCells) / (cellCount - solidCells)); + Game1.spriteBatch.Draw( + Game1.staminaRect, + new Rectangle(bundleBannerX + cell * cellWidth, bundleBannerY, cellWidth, 36), + Color.Crimson * fadeAmount + ); + } + + Game1.spriteBatch.Draw( + Game1.mouseCursors, + position, + _bundleIcon.sourceRect, + Color.White, + 0f, + Vector2.Zero, + _bundleIcon.scale, + SpriteEffects.None, + 0.86f + ); + + Game1.spriteBatch.DrawString( + Game1.dialogueFont, + bundleName, + position + new Vector2(_bundleIcon.sourceRect.Width * _bundleIcon.scale + 3, 0), + Color.White + ); + } + + private void DrawShippingBin(SpriteBatch b, Vector2 position, Vector2 origin) + { + var shippingBinOffset = new Vector2(0, 2); + // var shippingBinLidOffset = Vector2.Zero; + + // NB This is not the texture used to draw the shipping bin on the farm map. + // The one for the farm is located in "Buildings\Shipping Bin". + Game1.spriteBatch.Draw( + _shippingBottomIcon.texture, + position, + _shippingBottomIcon.sourceRect, + Color.White, + 0f, + origin - shippingBinOffset, + _shippingBottomIcon.scale, + SpriteEffects.None, + 0.86f + ); + Game1.spriteBatch.Draw( + _shippingTopIcon.texture, + position, + _shippingTopIcon.sourceRect, + Color.White, + 0f, + origin, + _shippingTopIcon.scale, + SpriteEffects.None, + 0.86f + ); + } + + private record ItemAndQuality(string qualifiedItemId, int quality); + } } diff --git a/UIInfoSuite2/UIElements/ShowQueenOfSauceIcon.cs b/UIInfoSuite2/UIElements/ShowQueenOfSauceIcon.cs index bef012a9..d43bbb5a 100644 --- a/UIInfoSuite2/UIElements/ShowQueenOfSauceIcon.cs +++ b/UIInfoSuite2/UIElements/ShowQueenOfSauceIcon.cs @@ -1,196 +1,160 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; using StardewValley.Objects; -using System; -using System.Collections.Generic; -using System.Reflection; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowQueenOfSauceIcon : IDisposable + internal class ShowQueenOfSauceIcon : IDisposable + { + private class QueenOfSauceTV : TV { - #region Properties - private Dictionary _recipesByDescription = new(); - private Dictionary _recipes = new(); - private CraftingRecipe _todaysRecipe; + public string[] GetWeeklyRecipe() + { + return base.getWeeklyRecipe(); + } + } - private readonly PerScreen _drawQueenOfSauceIcon = new(); - //private bool _drawDishOfDayIcon = false; - private readonly PerScreen _icon = new(); + #region Properties + private readonly Dictionary _recipesByDescription = new(); + private Dictionary _recipes = new(); + private CraftingRecipe _todaysRecipe; - private readonly IModHelper _helper; - #endregion + private readonly PerScreen _drawQueenOfSauceIcon = new(); - #region Life cycle - public ShowQueenOfSauceIcon(IModHelper helper) - { - _helper = helper; - } + //private bool _drawDishOfDayIcon = false; + private readonly PerScreen _icon = new(); - public void Dispose() - { - ToggleOption(false); - } + private readonly IModHelper _helper; + #endregion - public void ToggleOption(bool showQueenOfSauceIcon) - { - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - _helper.Events.GameLoop.SaveLoaded -= OnSaveLoaded; - - if (showQueenOfSauceIcon) - { - LoadRecipes(); - CheckForNewRecipe(); - - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - _helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; - } - } - #endregion + #region Life cycle + public ShowQueenOfSauceIcon(IModHelper helper) + { + _helper = helper; + } - #region Event subscriptions - private void OnDayStarted(object sender, DayStartedEventArgs e) - { - CheckForNewRecipe(); - } + public void Dispose() + { + ToggleOption(false); + } - private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) - { - CheckForNewRecipe(); - } + public void ToggleOption(bool showQueenOfSauceIcon) + { + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + _helper.Events.GameLoop.SaveLoaded -= OnSaveLoaded; + + if (showQueenOfSauceIcon) + { + LoadRecipes(); + CheckForNewRecipe(); + + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + _helper.Events.GameLoop.SaveLoaded += OnSaveLoaded; + } + } + #endregion - private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) - { - if (e.IsOneSecond && _drawQueenOfSauceIcon.Value && Game1.player.knowsRecipe(_todaysRecipe.name)) - _drawQueenOfSauceIcon.Value = false; - } + #region Event subscriptions + private void OnDayStarted(object sender, DayStartedEventArgs e) + { + CheckForNewRecipe(); + } - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - if (!Game1.eventUp) - { - if (_drawQueenOfSauceIcon.Value) - { - Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - - _icon.Value = new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), - Game1.mouseCursors, - new Rectangle(609, 361, 28, 28), - 1.3f); - _icon.Value.draw(Game1.spriteBatch); - } - - //if (_drawDishOfDayIcon) - //{ - // Point iconLocation = IconHandler.Handler.GetNewIconPosition(); - // float scale = 2.9f; - - // Game1.spriteBatch.Draw( - // Game1.objectSpriteSheet, - // new Vector2(iconLocation.X, iconLocation.Y), - // new Rectangle(306, 291, 14, 14), - // Color.White, - // 0, - // Vector2.Zero, - // scale, - // SpriteEffects.None, - // 1f); - - // ClickableTextureComponent texture = - // new ClickableTextureComponent( - // _gus.Name, - // new Rectangle( - // iconLocation.X - 7, - // iconLocation.Y - 2, - // (int)(16.0 * scale), - // (int)(16.0 * scale)), - // null, - // _gus.Name, - // _gus.Sprite.Texture, - // _gus.GetHeadShot(), - // 2f); - - // texture.draw(Game1.spriteBatch); - - // if (texture.containsPoint(Game1.getMouseX(), Game1.getMouseY())) - // { - // IClickableMenu.drawHoverText( - // Game1.spriteBatch, - // "Gus is selling " + Game1.dishOfTheDay.DisplayName + " recipe today!", - // Game1.dialogueFont); - // } - //} - } - } + private void OnSaveLoaded(object sender, SaveLoadedEventArgs e) + { + CheckForNewRecipe(); + } - private void OnRenderedHud(object sender, RenderedHudEventArgs e) - { - if (_drawQueenOfSauceIcon.Value && !Game1.IsFakedBlackScreen() && (_icon.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false)) - { - IClickableMenu.drawHoverText(Game1.spriteBatch, _helper.SafeGetString(LanguageKeys.TodaysRecipe) + _todaysRecipe.DisplayName, Game1.dialogueFont); - } - } - #endregion + private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + { + if (e.IsOneSecond && _drawQueenOfSauceIcon.Value && Game1.player.knowsRecipe(_todaysRecipe.name)) + { + _drawQueenOfSauceIcon.Value = false; + } + } - #region Logic - private void LoadRecipes() + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + if (!Game1.eventUp) + { + if (_drawQueenOfSauceIcon.Value) { - if (_recipes.Count == 0) - { - _recipes = Game1.content.Load>("Data\\TV\\CookingChannel"); - foreach (var next in _recipes) - { - string[] values = next.Value.Split('/'); - if (values.Length > 1) - { - _recipesByDescription[values[1]] = values[0]; - } - } - } + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + + _icon.Value = new ClickableTextureComponent( + new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + Game1.mouseCursors, + new Rectangle(609, 361, 28, 28), + 1.3f + ); + _icon.Value.draw(Game1.spriteBatch); } + } + } - private void CheckForNewRecipe() - { - int recipiesKnownBeforeTvCall = Game1.player.cookingRecipes.Count(); - string[] dialogue = typeof(TV).GetMethod("getWeeklyRecipe", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(new TV(), null) as string[]; - _todaysRecipe = new CraftingRecipe(_recipesByDescription.SafeGet(dialogue[0]), true); - - if (Game1.player.cookingRecipes.Count() > recipiesKnownBeforeTvCall) - Game1.player.cookingRecipes.Remove(_todaysRecipe.name); + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + if (_drawQueenOfSauceIcon.Value && + !Game1.IsFakedBlackScreen() && + (_icon.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false)) + { + IClickableMenu.drawHoverText( + Game1.spriteBatch, + _helper.SafeGetString(LanguageKeys.TodaysRecipe) + _todaysRecipe.DisplayName, + Game1.dialogueFont + ); + } + } + #endregion - _drawQueenOfSauceIcon.Value = (Game1.dayOfMonth % 7 == 0 || (Game1.dayOfMonth - 3) % 7 == 0) - && Game1.stats.DaysPlayed > 5 && !Game1.player.knowsRecipe(_todaysRecipe.name); + #region Logic + private void LoadRecipes() + { + if (_recipes.Count == 0) + { + _recipes = Game1.content.Load>("Data\\TV\\CookingChannel"); + foreach (KeyValuePair next in _recipes) + { + string[] values = next.Value.Split('/'); + if (values.Length > 1) + { + _recipesByDescription[values[1]] = values[0]; + } } + } + } - //private void FindGus() - //{ - // foreach (var location in Game1.locations) - // { - // foreach (var npc in location.characters) - // { - // if (npc.Name == "Gus") - // { - // _gus = npc; - // break; - // } - // } - // if (_gus != null) - // break; - // } - //} - #endregion + private void CheckForNewRecipe() + { + int recipiesKnownBeforeTvCall = Game1.player.cookingRecipes.Count(); + string[] dialogue = new QueenOfSauceTV().GetWeeklyRecipe(); + // TODO fix nullability reference + _todaysRecipe = new CraftingRecipe(_recipesByDescription.SafeGet(dialogue[0]), true); + + if (Game1.player.cookingRecipes.Count() > recipiesKnownBeforeTvCall) + { + Game1.player.cookingRecipes.Remove(_todaysRecipe.name); + } + + _drawQueenOfSauceIcon.Value = (Game1.dayOfMonth % 7 == 0 || (Game1.dayOfMonth - 3) % 7 == 0) && + Game1.stats.DaysPlayed > 5 && + !Game1.player.knowsRecipe(_todaysRecipe.name); } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowRainyDayIcon.cs b/UIInfoSuite2/UIElements/ShowRainyDayIcon.cs index 66b1d21e..8202a933 100644 --- a/UIInfoSuite2/UIElements/ShowRainyDayIcon.cs +++ b/UIInfoSuite2/UIElements/ShowRainyDayIcon.cs @@ -1,271 +1,260 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowRainyDayIcon : IDisposable + internal class ShowRainyDayIcon : IDisposable + { + #region Properties + private class LocationWeather { - #region Properties - - private class LocationWeather - { - internal bool IsRainyTomorrow { get; set; } - internal Rectangle? SpriteLocation { get; set; } - internal string HoverText { get; set; } - internal ClickableTextureComponent IconComponent { get; set; } - } - - private readonly LocationWeather _valleyWeather = new(); - private readonly LocationWeather _islandWeather = new(); - private Texture2D _iconSheet; - - private Color[] _weatherIconColors; - private const int WeatherSheetWidth = 81; - private const int WeatherSheetHeight = 18; - - private readonly IModHelper _helper; - - #endregion - - #region Lifecycle - - public ShowRainyDayIcon(IModHelper helper) - { - _helper = helper; - CreateTileSheet(); - } - - public void Dispose() - { - ToggleOption(false); - _iconSheet.Dispose(); - } - - public void ToggleOption(bool showRainyDay) - { - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - - if (showRainyDay) - { - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - } - } - - #endregion - - #region Event subscriptions - - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - GetWeatherIconSpriteLocation(); - - if (Game1.eventUp) - { - return; - } - - RenderLocationWeatherIcon(_valleyWeather); - if (HasVisitedIsland()) - { - RenderLocationWeatherIcon(_islandWeather); - } - } - - private void OnRenderedHud(object sender, RenderedHudEventArgs e) - { - // Show text on hover - RenderWeatherHoverText(_valleyWeather); - if (HasVisitedIsland()) - { - RenderWeatherHoverText(_islandWeather); - } - } - - private void RenderLocationWeatherIcon(LocationWeather weather) - { - // Escape if we don't have that weather or a sprite - if (!weather.IsRainyTomorrow || !weather.SpriteLocation.HasValue) return; - // Draw icon - var iconPosition = IconHandler.Handler.GetNewIconPosition(); - weather.IconComponent = - new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), - _iconSheet, - weather.SpriteLocation.Value, - 8 / 3f); - weather.IconComponent.draw(Game1.spriteBatch); - } - - private static void RenderWeatherHoverText(LocationWeather weather) - { - var hasMouse = weather.IconComponent?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false; - var hasText = !string.IsNullOrEmpty(weather.HoverText); - if (weather.IsRainyTomorrow && hasMouse && hasText) - { - IClickableMenu.drawHoverText( - Game1.spriteBatch, - weather.HoverText, - Game1.dialogueFont - ); - } - } - - #endregion - - #region Logic - - private static bool HasVisitedIsland() - { - return Game1.MasterPlayer.mailReceived.Contains("willyBoatFixed"); - } - - private static int GetWeatherForTomorrow() - { - var date = new WorldDate(Game1.Date); - ++date.TotalDays; - var tomorrowWeather = Game1.IsMasterGame - ? Game1.weatherForTomorrow - : Game1.netWorldState.Value.WeatherForTomorrow; - return Game1.getWeatherModificationsForDate(date, tomorrowWeather); - } - - private static int GetIslandWeatherForTomorrow() - { - return Game1.netWorldState.Value.GetWeatherForLocation(GameLocation.LocationContext.Island).weatherForTomorrow.Value; - } - - /// - /// Creates a custom tilesheet for weather icons. - /// Meant to mimic the TV screen, which has a border around it, while the individual icons in the Cursors tilesheet don't have a border - /// Extracts the border, and each individual weather icon and stitches them together into one separate sheet - /// - private void CreateTileSheet() - { - ModEntry.MonitorObject.Log("Setting up icon sheet", LogLevel.Info); - // Setup Texture sheet as a copy, so as not to disturb existing sprites - _iconSheet = new Texture2D(Game1.graphics.GraphicsDevice, WeatherSheetWidth, WeatherSheetHeight); - _weatherIconColors = new Color[WeatherSheetWidth * WeatherSheetHeight]; - var cursorColors = new Color[Game1.mouseCursors.Width * Game1.mouseCursors.Height]; - var bounds = new Rectangle(0, 0, Game1.mouseCursors.Width, Game1.mouseCursors.Height); - Game1.mouseCursors.GetData(cursorColors); - var subTextureColors = new Color[15 * 15]; - - // Copy over the bits we want - // Border from TV screen - Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(499, 307, 15, 15)); - // Copy to each destination - for (var i = 0; i < 3; i++) - { - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, - new Rectangle(i * 15, 0, 15, 15)); - } - - // Add in expanded sprites for the island parrot - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(45, 0, 15, 15)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(63, 0, 15, 15)); - - subTextureColors = new Color[13 * 13]; - // Rainy Weather - Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(504, 333, 13, 13)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(1, 1, 13, 13)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(46, 1, 13, 13)); - - // Stormy Weather - Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(426, 346, 13, 13)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(16, 1, 13, 13)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(64, 1, 13, 13)); - - // Snowy Weather - Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(465, 346, 13, 13)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(31, 1, 13, 13)); - - // Size of the parrot icon - subTextureColors = new Color[9 * 14]; - // Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(155, 148, 9, 14)); - Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(146, 149, 9, 14)); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(54, 4, 9, 14), - true); - Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(72, 4, 9, 14), - true); - - _iconSheet.SetData(_weatherIconColors); - } - - private void GetWeatherIconSpriteLocation() - { - SetValleyWeatherSprite(); - if (HasVisitedIsland()) - { - SetIslandWeatherSprite(); - } - } - - private void SetValleyWeatherSprite() - { - switch (GetWeatherForTomorrow()) - { - case Game1.weather_sunny: - case Game1.weather_debris: - case Game1.weather_festival: - case Game1.weather_wedding: - _valleyWeather.IsRainyTomorrow = false; - break; - - case Game1.weather_rain: - _valleyWeather.IsRainyTomorrow = true; - _valleyWeather.SpriteLocation = new Rectangle(0, 0, 15, 15); - _valleyWeather.HoverText = _helper.SafeGetString(LanguageKeys.RainNextDay); - break; - - case Game1.weather_lightning: - _valleyWeather.IsRainyTomorrow = true; - _valleyWeather.SpriteLocation = new Rectangle(15, 0, 15, 15); - _valleyWeather.HoverText = _helper.SafeGetString(LanguageKeys.ThunderstormNextDay); - break; - - case Game1.weather_snow: - _valleyWeather.IsRainyTomorrow = true; - _valleyWeather.SpriteLocation = new Rectangle(30, 0, 15, 15); - _valleyWeather.HoverText = _helper.SafeGetString(LanguageKeys.SnowNextDay); - break; - - default: - _valleyWeather.IsRainyTomorrow = false; - break; - } - } - - private void SetIslandWeatherSprite() - { - var islandWeather = GetIslandWeatherForTomorrow(); - switch (islandWeather) - { - case Game1.weather_rain: - _islandWeather.IsRainyTomorrow = true; - _islandWeather.SpriteLocation = new Rectangle(45, 0, 18, 18); - _islandWeather.HoverText = _helper.SafeGetString(LanguageKeys.IslandRainNextDay); - break; - - case Game1.weather_lightning: - _islandWeather.IsRainyTomorrow = true; - _islandWeather.SpriteLocation = new Rectangle(63, 0, 18, 18); - _islandWeather.HoverText = _helper.SafeGetString(LanguageKeys.IslandThunderstormNextDay); - break; - default: - _islandWeather.IsRainyTomorrow = false; - break; - } - } - - #endregion + internal bool IsRainyTomorrow { get; set; } + internal Rectangle? SpriteLocation { get; set; } + internal string HoverText { get; set; } + internal ClickableTextureComponent IconComponent { get; set; } } + + private readonly LocationWeather _valleyWeather = new(); + private readonly LocationWeather _islandWeather = new(); + private Texture2D _iconSheet; + + private Color[] _weatherIconColors; + private const int WeatherSheetWidth = 81; + private const int WeatherSheetHeight = 18; + + private readonly IModHelper _helper; + #endregion + + #region Lifecycle + public ShowRainyDayIcon(IModHelper helper) + { + _helper = helper; + CreateTileSheet(); + } + + public void Dispose() + { + ToggleOption(false); + _iconSheet.Dispose(); + } + + public void ToggleOption(bool showRainyDay) + { + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + + if (showRainyDay) + { + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + } + } + #endregion + + #region Event subscriptions + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + GetWeatherIconSpriteLocation(); + + if (Game1.eventUp) + { + return; + } + + RenderLocationWeatherIcon(_valleyWeather); + if (HasVisitedIsland()) + { + RenderLocationWeatherIcon(_islandWeather); + } + } + + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + // Show text on hover + RenderWeatherHoverText(_valleyWeather); + if (HasVisitedIsland()) + { + RenderWeatherHoverText(_islandWeather); + } + } + + private void RenderLocationWeatherIcon(LocationWeather weather) + { + // Escape if we don't have that weather or a sprite + if (!weather.IsRainyTomorrow || !weather.SpriteLocation.HasValue) + { + return; + } + + // Draw icon + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + weather.IconComponent = new ClickableTextureComponent( + new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + _iconSheet, + weather.SpriteLocation.Value, + 8 / 3f + ); + weather.IconComponent.draw(Game1.spriteBatch); + } + + private static void RenderWeatherHoverText(LocationWeather weather) + { + bool hasMouse = weather.IconComponent?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false; + bool hasText = !string.IsNullOrEmpty(weather.HoverText); + if (weather.IsRainyTomorrow && hasMouse && hasText) + { + IClickableMenu.drawHoverText(Game1.spriteBatch, weather.HoverText, Game1.dialogueFont); + } + } + #endregion + + #region Logic + private static bool HasVisitedIsland() + { + return Game1.MasterPlayer.mailReceived.Contains("willyBoatFixed"); + } + + private static string GetWeatherForTomorrow() + { + var date = new WorldDate(Game1.Date); + ++date.TotalDays; + string tomorrowWeather = Game1.IsMasterGame + ? Game1.weatherForTomorrow + : Game1.netWorldState.Value.WeatherForTomorrow; + return Game1.getWeatherModificationsForDate(date, tomorrowWeather); + } + + private static string GetIslandWeatherForTomorrow() + { + return Game1.netWorldState.Value.GetWeatherForLocation("Island").WeatherForTomorrow; + } + + /// + /// Creates a custom tilesheet for weather icons. + /// Meant to mimic the TV screen, which has a border around it, while the individual icons in the Cursors tilesheet + /// don't have a border + /// Extracts the border, and each individual weather icon and stitches them together into one separate sheet + /// + private void CreateTileSheet() + { + ModEntry.MonitorObject.Log("Setting up icon sheet", LogLevel.Info); + // Setup Texture sheet as a copy, so as not to disturb existing sprites + _iconSheet = new Texture2D(Game1.graphics.GraphicsDevice, WeatherSheetWidth, WeatherSheetHeight); + _weatherIconColors = new Color[WeatherSheetWidth * WeatherSheetHeight]; + var cursorColors = new Color[Game1.mouseCursors.Width * Game1.mouseCursors.Height]; + var bounds = new Rectangle(0, 0, Game1.mouseCursors.Width, Game1.mouseCursors.Height); + Game1.mouseCursors.GetData(cursorColors); + var subTextureColors = new Color[15 * 15]; + + // Copy over the bits we want + // Border from TV screen + Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(499, 307, 15, 15)); + // Copy to each destination + for (var i = 0; i < 3; i++) + { + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(i * 15, 0, 15, 15)); + } + + // Add in expanded sprites for the island parrot + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(45, 0, 15, 15)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(63, 0, 15, 15)); + + subTextureColors = new Color[13 * 13]; + // Rainy Weather + Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(504, 333, 13, 13)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(1, 1, 13, 13)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(46, 1, 13, 13)); + + // Stormy Weather + Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(426, 346, 13, 13)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(16, 1, 13, 13)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(64, 1, 13, 13)); + + // Snowy Weather + Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(465, 346, 13, 13)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(31, 1, 13, 13)); + + // Size of the parrot icon + subTextureColors = new Color[9 * 14]; + // Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(155, 148, 9, 14)); + Tools.GetSubTexture(subTextureColors, cursorColors, bounds, new Rectangle(146, 149, 9, 14)); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(54, 4, 9, 14), true); + Tools.SetSubTexture(subTextureColors, _weatherIconColors, WeatherSheetWidth, new Rectangle(72, 4, 9, 14), true); + + _iconSheet.SetData(_weatherIconColors); + } + + private void GetWeatherIconSpriteLocation() + { + SetValleyWeatherSprite(); + if (HasVisitedIsland()) + { + SetIslandWeatherSprite(); + } + } + + private void SetValleyWeatherSprite() + { + switch (GetWeatherForTomorrow()) + { + case Game1.weather_sunny: + case Game1.weather_debris: + case Game1.weather_festival: + case Game1.weather_wedding: + _valleyWeather.IsRainyTomorrow = false; + break; + + case Game1.weather_rain: + _valleyWeather.IsRainyTomorrow = true; + _valleyWeather.SpriteLocation = new Rectangle(0, 0, 15, 15); + _valleyWeather.HoverText = _helper.SafeGetString(LanguageKeys.RainNextDay); + break; + + case Game1.weather_lightning: + _valleyWeather.IsRainyTomorrow = true; + _valleyWeather.SpriteLocation = new Rectangle(15, 0, 15, 15); + _valleyWeather.HoverText = _helper.SafeGetString(LanguageKeys.ThunderstormNextDay); + break; + + case Game1.weather_snow: + _valleyWeather.IsRainyTomorrow = true; + _valleyWeather.SpriteLocation = new Rectangle(30, 0, 15, 15); + _valleyWeather.HoverText = _helper.SafeGetString(LanguageKeys.SnowNextDay); + break; + + default: + _valleyWeather.IsRainyTomorrow = false; + break; + } + } + + private void SetIslandWeatherSprite() + { + switch (GetIslandWeatherForTomorrow()) + { + case Game1.weather_rain: + _islandWeather.IsRainyTomorrow = true; + _islandWeather.SpriteLocation = new Rectangle(45, 0, 18, 18); + _islandWeather.HoverText = _helper.SafeGetString(LanguageKeys.IslandRainNextDay); + break; + + case Game1.weather_lightning: + _islandWeather.IsRainyTomorrow = true; + _islandWeather.SpriteLocation = new Rectangle(63, 0, 18, 18); + _islandWeather.HoverText = _helper.SafeGetString(LanguageKeys.IslandThunderstormNextDay); + break; + default: + _islandWeather.IsRainyTomorrow = false; + break; + } + } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowRobinBuildingStatusIcon.cs b/UIInfoSuite2/UIElements/ShowRobinBuildingStatusIcon.cs index 9eca7c08..ecccc845 100644 --- a/UIInfoSuite2/UIElements/ShowRobinBuildingStatusIcon.cs +++ b/UIInfoSuite2/UIElements/ShowRobinBuildingStatusIcon.cs @@ -1,4 +1,6 @@ -using Microsoft.Xna.Framework; +using System; +using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; @@ -6,154 +8,154 @@ using StardewValley; using StardewValley.Buildings; using StardewValley.Menus; -using System; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowRobinBuildingStatusIcon : IDisposable + internal class ShowRobinBuildingStatusIcon : IDisposable + { + #region Properties + private bool _IsBuildingInProgress; + private Rectangle? _buildingIconSpriteLocation; + private string _hoverText; + private readonly PerScreen _buildingIcon = new(); + private Texture2D _robinIconSheet; + + private readonly IModHelper _helper; + #endregion + + #region Lifecycle + public ShowRobinBuildingStatusIcon(IModHelper helper) { - #region Properties - - private bool _IsBuildingInProgress; - private Rectangle? _buildingIconSpriteLocation; - private string _hoverText; - private PerScreen _buildingIcon = new(); - private Texture2D _robinIconSheet; + _helper = helper; + } - private readonly IModHelper _helper; - #endregion + public void Dispose() + { + ToggleOption(false); + } - #region Lifecycle - public ShowRobinBuildingStatusIcon(IModHelper helper) - { - _helper = helper; - } + public void ToggleOption(bool showRobinBuildingStatus) + { + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + + if (showRobinBuildingStatus) + { + UpdateRobinBuindingStatusData(); + + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + } + } + #endregion - public void Dispose() - { - ToggleOption(false); - } + #region Event subscriptions + private void OnDayStarted(object sender, DayStartedEventArgs e) + { + UpdateRobinBuindingStatusData(); + } - public void ToggleOption(bool showRobinBuildingStatus) - { - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - - if (showRobinBuildingStatus) - { - UpdateRobinBuindingStatusData(); - - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - } - } - #endregion + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + // Draw icon + if (!Game1.eventUp && _IsBuildingInProgress && _buildingIconSpriteLocation.HasValue) + { + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + _buildingIcon.Value = new ClickableTextureComponent( + new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + _robinIconSheet, + _buildingIconSpriteLocation.Value, + 8 / 3f + ); + _buildingIcon.Value.draw(Game1.spriteBatch); + } + } - #region Event subscriptions - private void OnDayStarted(object sender, DayStartedEventArgs e) - { - UpdateRobinBuindingStatusData(); - } + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + // Show text on hover + if (_IsBuildingInProgress && + (_buildingIcon.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false) && + !string.IsNullOrEmpty(_hoverText)) + { + IClickableMenu.drawHoverText(Game1.spriteBatch, _hoverText, Game1.dialogueFont); + } + } + #endregion - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - // Draw icon - if (!Game1.eventUp && _IsBuildingInProgress && _buildingIconSpriteLocation.HasValue) - { - Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - _buildingIcon.Value = - new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), - _robinIconSheet, - _buildingIconSpriteLocation.Value, - 8 / 3f); - _buildingIcon.Value.draw(Game1.spriteBatch); - } - } + #region Logic + private bool GetRobinMessage(out string hoverText) + { + int remainingDays = Game1.player.daysUntilHouseUpgrade.Value; - private void OnRenderedHud(object sender, RenderedHudEventArgs e) - { - // Show text on hover - if (_IsBuildingInProgress && (_buildingIcon.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false) && !String.IsNullOrEmpty(_hoverText)) - { - IClickableMenu.drawHoverText( - Game1.spriteBatch, - _hoverText, - Game1.dialogueFont - ); - } - } - #endregion + if (remainingDays <= 0) + { + Building? building = Game1.getFarm().buildings.FirstOrDefault(b => b.isUnderConstruction(false)); - #region Logic - private bool GetRobinMessage(out string hoverText) + if (building is not null) { - int remainingDays = Game1.player.daysUntilHouseUpgrade.Value; - - if (remainingDays <= 0) - { - Building b = Game1.getFarm().getBuildingUnderConstruction(); - - if (b is not null) - { - if (b.daysOfConstructionLeft.Value > b.daysUntilUpgrade.Value) - { - hoverText = String.Format(_helper.SafeGetString(LanguageKeys.RobinBuildingStatus), b.daysOfConstructionLeft.Value); - return true; - } - else - { - // Add another translation string for this? - hoverText = String.Format(_helper.SafeGetString(LanguageKeys.RobinBuildingStatus), b.daysUntilUpgrade.Value); - return true; - } - } - else - { - hoverText = String.Empty; - return false; - } - } - - hoverText = String.Format(_helper.SafeGetString(LanguageKeys.RobinHouseUpgradeStatus), remainingDays); + if (building.daysOfConstructionLeft.Value > building.daysUntilUpgrade.Value) + { + hoverText = string.Format( + _helper.SafeGetString(LanguageKeys.RobinBuildingStatus), + building.daysOfConstructionLeft.Value + ); return true; + } + + // Add another translation string for this? + hoverText = string.Format( + _helper.SafeGetString(LanguageKeys.RobinBuildingStatus), + building.daysUntilUpgrade.Value + ); + return true; } - private void UpdateRobinBuindingStatusData() - { - if (GetRobinMessage(out _hoverText)) - { - _IsBuildingInProgress = true; - FindRobinSpritesheet(); - } - else - { - _IsBuildingInProgress = false; - } - } + hoverText = string.Empty; + return false; + } - private void FindRobinSpritesheet() - { - Texture2D? foundTexture = Game1.getCharacterFromName("Robin")?.Sprite?.Texture; - if (foundTexture != null) - { - _robinIconSheet = foundTexture; - } - else - { - ModEntry.MonitorObject.Log($"{this.GetType().Name}: Could not find Robin spritesheet.", LogLevel.Warn); - } - if (_robinIconSheet == null) - { - ModEntry.MonitorObject.Log($"{this.GetType().Name}: Could not find Robin spritesheet.", LogLevel.Warn); - } - - _buildingIconSpriteLocation = new Rectangle(0, 195 + 1, 15, 15 - 1); // 1px edits for better alignment with other icons - } - #endregion + hoverText = string.Format(_helper.SafeGetString(LanguageKeys.RobinHouseUpgradeStatus), remainingDays); + return true; + } + + private void UpdateRobinBuindingStatusData() + { + if (GetRobinMessage(out _hoverText)) + { + _IsBuildingInProgress = true; + FindRobinSpritesheet(); + } + else + { + _IsBuildingInProgress = false; + } + } + + private void FindRobinSpritesheet() + { + Texture2D? foundTexture = Game1.getCharacterFromName("Robin")?.Sprite?.Texture; + if (foundTexture != null) + { + _robinIconSheet = foundTexture; + } + else + { + ModEntry.MonitorObject.Log($"{GetType().Name}: Could not find Robin spritesheet.", LogLevel.Warn); + } + + if (_robinIconSheet == null) + { + ModEntry.MonitorObject.Log($"{GetType().Name}: Could not find Robin spritesheet.", LogLevel.Warn); + } + + _buildingIconSpriteLocation = + new Rectangle(0, 195 + 1, 15, 15 - 1); // 1px edits for better alignment with other icons } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowSeasonalBerry.cs b/UIInfoSuite2/UIElements/ShowSeasonalBerry.cs index 706c70a6..74859b9e 100644 --- a/UIInfoSuite2/UIElements/ShowSeasonalBerry.cs +++ b/UIInfoSuite2/UIElements/ShowSeasonalBerry.cs @@ -1,138 +1,128 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowSeasonalBerry : IDisposable + internal class ShowSeasonalBerry : IDisposable + { + #region Logic + private void UpdateBerryForDay() { - #region Properties - - private Rectangle? _berrySpriteLocation; - private float _spriteScale = 8 / 3f; - private string _hoverText; - private ClickableTextureComponent _berryIcon; - - private readonly IModHelper _helper; - - private bool Enabled { get; set; } - private bool ShowHazelnut { get; set; } - - #endregion - - #region Lifecycle - - public ShowSeasonalBerry(IModHelper helper) - { - _helper = helper; - } - - public void Dispose() - { - ToggleOption(false); - } - - public void ToggleOption(bool showSeasonalBerry) - { - Enabled = showSeasonalBerry; - - _berrySpriteLocation = null; - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; + string? season = Game1.currentSeason; + int day = Game1.dayOfMonth; + switch (season) + { + case "spring" when day is >= 15 and <= 18: + _berrySpriteLocation = new Rectangle(128, 193, 15, 15); + _hoverText = _helper.SafeGetString(LanguageKeys.CanFindSalmonberry); + _spriteScale = 8 / 3f; + break; + case "fall" when day is >= 8 and <= 11: + _berrySpriteLocation = new Rectangle(32, 272, 16, 16); + _hoverText = _helper.SafeGetString(LanguageKeys.CanFindBlackberry); + _spriteScale = 5 / 2f; + break; + case "fall" when day >= 15 && ShowHazelnut: + _berrySpriteLocation = new Rectangle(1, 274, 14, 14); + _hoverText = _helper.SafeGetString(LanguageKeys.CanFindHazelnut); + _spriteScale = 20 / 7f; + break; + default: + _berrySpriteLocation = null; + break; + } + } + #endregion - if (showSeasonalBerry) - { - UpdateBerryForDay(); + #region Properties + private Rectangle? _berrySpriteLocation; + private float _spriteScale = 8 / 3f; + private string _hoverText; + private ClickableTextureComponent _berryIcon; - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - } - } + private readonly IModHelper _helper; - public void ToggleHazelnutOption(bool showHazelnut) - { - ShowHazelnut = showHazelnut; - ToggleOption(Enabled); - } + private bool Enabled { get; set; } + private bool ShowHazelnut { get; set; } + #endregion - #endregion + #region Lifecycle + public ShowSeasonalBerry(IModHelper helper) + { + _helper = helper; + } - #region Event subscriptions + public void Dispose() + { + ToggleOption(false); + } - private void OnDayStarted(object sender, DayStartedEventArgs e) - { - UpdateBerryForDay(); - } + public void ToggleOption(bool showSeasonalBerry) + { + Enabled = showSeasonalBerry; - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - // Draw icon - if (Game1.eventUp || !_berrySpriteLocation.HasValue) return; + _berrySpriteLocation = null; + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; - var iconPosition = IconHandler.Handler.GetNewIconPosition(); - _berryIcon = - new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), - Game1.objectSpriteSheet, - _berrySpriteLocation.Value, - _spriteScale - ); - _berryIcon.draw(Game1.spriteBatch); - } + if (showSeasonalBerry) + { + UpdateBerryForDay(); - private void OnRenderedHud(object sender, RenderedHudEventArgs e) - { - // Show text on hover - var hasMouse = _berryIcon?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false; - var hasText = !string.IsNullOrEmpty(_hoverText); - if (_berrySpriteLocation.HasValue && hasMouse && hasText) - { - IClickableMenu.drawHoverText( - Game1.spriteBatch, - _hoverText, - Game1.dialogueFont - ); - } - } + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + } + } - #endregion + public void ToggleHazelnutOption(bool showHazelnut) + { + ShowHazelnut = showHazelnut; + ToggleOption(Enabled); + } + #endregion - #region Logic + #region Event subscriptions + private void OnDayStarted(object sender, DayStartedEventArgs e) + { + UpdateBerryForDay(); + } - private void UpdateBerryForDay() - { - var season = Game1.currentSeason; - var day = Game1.dayOfMonth; - switch (season) - { - case "spring" when day is >= 15 and <= 18: - _berrySpriteLocation = new Rectangle(128, 193, 15, 15); - _hoverText = _helper.SafeGetString(LanguageKeys.CanFindSalmonberry); - _spriteScale = 8 / 3f; - break; - case "fall" when day is >= 8 and <= 11: - _berrySpriteLocation = new Rectangle(32, 272, 16, 16); - _hoverText = _helper.SafeGetString(LanguageKeys.CanFindBlackberry); - _spriteScale = 5 / 2f; - break; - case "fall" when day >= 15 && ShowHazelnut: - _berrySpriteLocation = new Rectangle(1, 274, 14, 14); - _hoverText = _helper.SafeGetString(LanguageKeys.CanFindHazelnut); - _spriteScale = 20 / 7f; - break; - default: - _berrySpriteLocation = null; - break; - } - } + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + // Draw icon + if (Game1.eventUp || !_berrySpriteLocation.HasValue) + { + return; + } + + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + _berryIcon = new ClickableTextureComponent( + new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + Game1.objectSpriteSheet, + _berrySpriteLocation.Value, + _spriteScale + ); + _berryIcon.draw(Game1.spriteBatch); + } - #endregion + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + // Show text on hover + bool hasMouse = _berryIcon?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false; + bool hasText = !string.IsNullOrEmpty(_hoverText); + if (_berrySpriteLocation.HasValue && hasMouse && hasText) + { + IClickableMenu.drawHoverText(Game1.spriteBatch, _hoverText, Game1.dialogueFont); + } } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowTodaysGifts.cs b/UIInfoSuite2/UIElements/ShowTodaysGifts.cs index 7adefda4..072eee83 100644 --- a/UIInfoSuite2/UIElements/ShowTodaysGifts.cs +++ b/UIInfoSuite2/UIElements/ShowTodaysGifts.cs @@ -1,118 +1,124 @@ -using Microsoft.Xna.Framework; +using System; +using System.Reflection; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; -using System; -using System.Linq; -using System.Reflection; namespace UIInfoSuite2.UIElements { - internal class ShowTodaysGifts : IDisposable + internal class ShowTodaysGifts : IDisposable + { + #region Properties + private SocialPage? _socialPage; + private readonly IModHelper _helper; + #endregion + + #region Lifecycle + public ShowTodaysGifts(IModHelper helper) { - #region Properties - private string[] _friendNames; - private SocialPage _socialPage; - private readonly IModHelper _helper; - #endregion + _helper = helper; + } - #region Lifecycle - public ShowTodaysGifts(IModHelper helper) - { - _helper = helper; - } + public void Dispose() + { + ToggleOption(false); + } - public void Dispose() - { - ToggleOption(false); - } + public void ToggleOption(bool showTodaysGift) + { + _helper.Events.Display.MenuChanged -= OnMenuChanged; + _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; - public void ToggleOption(bool showTodaysGift) - { - _helper.Events.Display.MenuChanged -= OnMenuChanged; - _helper.Events.Display.RenderedActiveMenu -= OnRenderedActiveMenu; + if (showTodaysGift) + { + _helper.Events.Display.MenuChanged += OnMenuChanged; + _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu; + } + } + #endregion - if (showTodaysGift) - { - _helper.Events.Display.MenuChanged += OnMenuChanged; - _helper.Events.Display.RenderedActiveMenu += OnRenderedActiveMenu; - } - } - #endregion + #region Event subscriptions + private void OnRenderedActiveMenu(object? sender, RenderedActiveMenuEventArgs e) + { + if (_socialPage == null) + { + GetSocialPage(); + return; + } - #region Event subscriptions - private void OnRenderedActiveMenu(object sender, RenderedActiveMenuEventArgs e) - { - if (_socialPage == null) - { - ExtendMenuIfNeeded(); - return; - } + if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == GameMenu.socialTab) + { + DrawTodaysGifts(); - if (Game1.activeClickableMenu is GameMenu gameMenu && gameMenu.currentTab == 2) - { - DrawTodaysGifts(); + string hoverText = gameMenu.hoverText; + IClickableMenu.drawHoverText(Game1.spriteBatch, hoverText, Game1.smallFont); + } + } - string hoverText = gameMenu.hoverText; - IClickableMenu.drawHoverText( - Game1.spriteBatch, - hoverText, - Game1.smallFont); - } - } + private void OnMenuChanged(object? sender, MenuChangedEventArgs e) + { + GetSocialPage(); + } + #endregion - private void OnMenuChanged(object sender, MenuChangedEventArgs e) - { - ExtendMenuIfNeeded(); - } - #endregion + #region Logic + private void GetSocialPage() + { + if (Game1.activeClickableMenu is not GameMenu gameMenu) + { + return; + } - #region Logic - private void ExtendMenuIfNeeded() + foreach (IClickableMenu? menu in gameMenu.pages) + { + if (menu is not SocialPage page) { - if (Game1.activeClickableMenu is GameMenu gameMenu) - { - foreach (var menu in gameMenu.pages) - { - if (menu is SocialPage page) - { - _socialPage = page; - _friendNames = _socialPage.names - .Select(name => name.ToString()) - .ToArray(); - break; - } - } - } + continue; } - private void DrawTodaysGifts() - { - int slotPosition = (int)typeof(SocialPage) - .GetField( - "slotPosition", - BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(_socialPage); - int yOffset = 25; + _socialPage = page; + break; + } + } - for (int i = slotPosition; i < slotPosition + 5 && i < _friendNames.Length; ++i) - { - int yPosition = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; - yOffset += 112; - string nextName = _friendNames[i]; - if (_socialPage.getFriendship(nextName).GiftsToday != 0 && _socialPage.getFriendship(nextName).GiftsThisWeek < 2) - { - Game1.spriteBatch.Draw( - Game1.mouseCursors, - new Vector2(_socialPage.xPositionOnScreen + 384 + 296 + 4, yPosition + 6), - new Rectangle?(new Rectangle(106, 442, 9, 9)), - Color.LightGray, 0.0f, Vector2.Zero, 3f, SpriteEffects.None, 0.22f - ); - } - } + private void DrawTodaysGifts() + { + if (_socialPage == null) + { + return; + } + + var slotPosition = (int)typeof(SocialPage) + .GetField("slotPosition", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(_socialPage); + var yOffset = 25; + + for (int i = slotPosition; i < slotPosition + 5 && i < _socialPage.SocialEntries.Count; ++i) + { + int yPosition = Game1.activeClickableMenu.yPositionOnScreen + 130 + yOffset; + yOffset += 112; + string internalName = _socialPage.SocialEntries[i].InternalName; + if (Game1.player.friendshipData.TryGetValue(internalName, out Friendship? data) && + data.GiftsToday != 0 && + data.GiftsThisWeek < 2) + { + Game1.spriteBatch.Draw( + Game1.mouseCursors, + new Vector2(_socialPage.xPositionOnScreen + 384 + 296 + 4, yPosition + 6), + new Rectangle(106, 442, 9, 9), + Color.LightGray, + 0.0f, + Vector2.Zero, + 3f, + SpriteEffects.None, + 0.22f + ); } - #endregion + } } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowToolUpgradeStatus.cs b/UIInfoSuite2/UIElements/ShowToolUpgradeStatus.cs index 88088e42..f913afec 100644 --- a/UIInfoSuite2/UIElements/ShowToolUpgradeStatus.cs +++ b/UIInfoSuite2/UIElements/ShowToolUpgradeStatus.cs @@ -1,159 +1,172 @@ -using Microsoft.Xna.Framework; +using System; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; using StardewValley.Tools; -using System; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - internal class ShowToolUpgradeStatus : IDisposable + internal class ShowToolUpgradeStatus : IDisposable + { + #region Logic + private void UpdateToolInfo() { - #region Properties - private readonly PerScreen _toolTexturePosition = new(); - private readonly PerScreen _hoverText = new(); - private readonly PerScreen _toolBeingUpgraded = new(); - private readonly PerScreen _toolUpgradeIcon = new(); + Tool toolBeingUpgraded = _toolBeingUpgraded.Value = Game1.player.toolBeingUpgraded.Value; + + if (toolBeingUpgraded == null) + { + return; + } + + if (toolBeingUpgraded is (Axe or Pickaxe or Hoe or WateringCan) || + (toolBeingUpgraded is GenericTool trashcan && trashcan.IndexOfMenuItemView is (>= 13 and <= 16))) + { + // NB The previous method used Tool.UpgradeLevel, but it turns out that field is not correctly set by the game. + // Tools other than the Trash Cans only worked because they had special handling code. + + // Read the 16x16 source rectangle based on Tool.IndexOfMenuItemView + _toolTexturePosition.Value = Game1.getSquareSourceRectForNonStandardTileSheet( + Game1.toolSpriteSheet, + 16, + 16, + toolBeingUpgraded.IndexOfMenuItemView + ); + } + else + { + _toolTexturePosition.Value = null; + } + + if (Game1.player.daysLeftForToolUpgrade.Value > 0) + { + _hoverText.Value = string.Format( + _helper.SafeGetString(LanguageKeys.DaysUntilToolIsUpgraded), + Game1.player.daysLeftForToolUpgrade.Value, + toolBeingUpgraded.DisplayName + ); + } + else + { + _hoverText.Value = string.Format( + _helper.SafeGetString(LanguageKeys.ToolIsFinishedBeingUpgraded), + toolBeingUpgraded.DisplayName + ); + } + } + #endregion - private readonly IModHelper _helper; - #endregion + #region Properties + private readonly PerScreen _toolTexturePosition = new(); + private readonly PerScreen _hoverText = new(); + private readonly PerScreen _toolBeingUpgraded = new(); + private readonly PerScreen _toolUpgradeIcon = new(); + private readonly IModHelper _helper; + #endregion - #region Life cycle - public ShowToolUpgradeStatus(IModHelper helper) - { - _helper = helper; - } - public void Dispose() - { - ToggleOption(false); - _toolBeingUpgraded.Value = null; - } + #region Life cycle + public ShowToolUpgradeStatus(IModHelper helper) + { + _helper = helper; + } - public void ToggleOption(bool showToolUpgradeStatus) - { - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; - - if (showToolUpgradeStatus) - { - UpdateToolInfo(); - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; - } - } - #endregion + public void Dispose() + { + ToggleOption(false); + _toolBeingUpgraded.Value = null; + } + public void ToggleOption(bool showToolUpgradeStatus) + { + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.GameLoop.UpdateTicked -= OnUpdateTicked; + + if (showToolUpgradeStatus) + { + UpdateToolInfo(); + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.GameLoop.UpdateTicked += OnUpdateTicked; + } + } + #endregion - #region Event subscriptions - private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) - { - if (e.IsOneSecond && _toolBeingUpgraded.Value != Game1.player.toolBeingUpgraded.Value) - UpdateToolInfo(); - } - private void OnDayStarted(object sender, DayStartedEventArgs e) - { - UpdateToolInfo(); - } + #region Event subscriptions + private void OnUpdateTicked(object sender, UpdateTickedEventArgs e) + { + if (e.IsOneSecond && _toolBeingUpgraded.Value != Game1.player.toolBeingUpgraded.Value) + { + UpdateToolInfo(); + } + } - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - // Draw a 40x40 icon - if (!Game1.eventUp && _toolBeingUpgraded.Value != null) - { - Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - _toolUpgradeIcon.Value = new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), - Game1.toolSpriteSheet, - new Rectangle(), - 2.5f); - - if (_toolTexturePosition.Value is Rectangle toolSourceRect) - { - _toolUpgradeIcon.Value.sourceRect = toolSourceRect; - _toolUpgradeIcon.Value.draw(e.SpriteBatch); - } - else - { - // Generic method for modded tools - try - { - // drawInMenu draws a 64x64 texture (16x16 texture at scale 4 = pixelZoom) if scaleSize is set to 1. - // It aligns position + (32, 32) with the center of the texture but we want to align position + 20, so that's an offset of -12. - _toolBeingUpgraded.Value.drawInMenu(e.SpriteBatch, iconPosition.ToVector2() + new Vector2(-12), 2.5f / Game1.pixelZoom); - } - catch (Exception ex) - { - ModEntry.MonitorObject.LogOnce($"An error occured while displaying the {_toolBeingUpgraded.Value.Name} tool.", LogLevel.Error); - ModEntry.MonitorObject.Log(ex.ToString()); - } - } - } - } + private void OnDayStarted(object sender, DayStartedEventArgs e) + { + UpdateToolInfo(); + } - private void OnRenderedHud(object sender, RenderedHudEventArgs e) + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + // Draw a 40x40 icon + if (!Game1.eventUp && _toolBeingUpgraded.Value != null) + { + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + _toolUpgradeIcon.Value = new ClickableTextureComponent( + new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + Game1.toolSpriteSheet, + new Rectangle(), + 2.5f + ); + + if (_toolTexturePosition.Value is Rectangle toolSourceRect) { - // Show text on hover - if (_toolBeingUpgraded.Value != null && (_toolUpgradeIcon.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false)) - { - IClickableMenu.drawHoverText( - Game1.spriteBatch, - _hoverText.Value, - Game1.dialogueFont - ); - } + _toolUpgradeIcon.Value.sourceRect = toolSourceRect; + _toolUpgradeIcon.Value.draw(e.SpriteBatch); } - #endregion - - - #region Logic - private void UpdateToolInfo() + else { - Tool toolBeingUpgraded = _toolBeingUpgraded.Value = Game1.player.toolBeingUpgraded.Value; - - if (toolBeingUpgraded == null) - { - return; - } - - if (toolBeingUpgraded is (Axe or Pickaxe or Hoe or WateringCan) - || toolBeingUpgraded is GenericTool trashcan && trashcan.IndexOfMenuItemView is (>= 13 and <= 16)) - { - // NB The previous method used Tool.UpgradeLevel, but it turns out that field is not correctly set by the game. - // Tools other than the Trash Cans only worked because they had special handling code. - - // Read the 16x16 source rectangle based on Tool.IndexOfMenuItemView - _toolTexturePosition.Value = Game1.getSquareSourceRectForNonStandardTileSheet( - Game1.toolSpriteSheet, - 16, 16, - toolBeingUpgraded.IndexOfMenuItemView); - } - else - { - _toolTexturePosition.Value = null; - } - - if (Game1.player.daysLeftForToolUpgrade.Value > 0) - { - _hoverText.Value = string.Format(_helper.SafeGetString(LanguageKeys.DaysUntilToolIsUpgraded), - Game1.player.daysLeftForToolUpgrade.Value, toolBeingUpgraded.DisplayName); - } - else - { - _hoverText.Value = string.Format(_helper.SafeGetString(LanguageKeys.ToolIsFinishedBeingUpgraded), - toolBeingUpgraded.DisplayName); - } + // Generic method for modded tools + try + { + // drawInMenu draws a 64x64 texture (16x16 texture at scale 4 = pixelZoom) if scaleSize is set to 1. + // It aligns position + (32, 32) with the center of the texture but we want to align position + 20, so that's an offset of -12. + _toolBeingUpgraded.Value.drawInMenu( + e.SpriteBatch, + iconPosition.ToVector2() + new Vector2(-12), + 2.5f / Game1.pixelZoom + ); + } + catch (Exception ex) + { + ModEntry.MonitorObject.LogOnce( + $"An error occured while displaying the {_toolBeingUpgraded.Value.Name} tool.", + LogLevel.Error + ); + ModEntry.MonitorObject.Log(ex.ToString()); + } } - #endregion + } + } + + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + // Show text on hover + if (_toolBeingUpgraded.Value != null && + (_toolUpgradeIcon.Value?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false)) + { + IClickableMenu.drawHoverText(Game1.spriteBatch, _hoverText.Value, Game1.dialogueFont); + } } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowTravelingMerchant.cs b/UIInfoSuite2/UIElements/ShowTravelingMerchant.cs index 6d92cc64..e22fb3f8 100644 --- a/UIInfoSuite2/UIElements/ShowTravelingMerchant.cs +++ b/UIInfoSuite2/UIElements/ShowTravelingMerchant.cs @@ -1,127 +1,123 @@ -using Microsoft.Xna.Framework; +using System; +using System.Linq; +using Microsoft.Xna.Framework; using StardewModdingAPI; using StardewModdingAPI.Events; using StardewValley; using StardewValley.Menus; using StardewValley.Objects; -using System; -using System.Linq; using UIInfoSuite2.Infrastructure; using UIInfoSuite2.Infrastructure.Extensions; namespace UIInfoSuite2.UIElements { - public class ShowTravelingMerchant : IDisposable + public class ShowTravelingMerchant : IDisposable + { + #region Properties + private bool _travelingMerchantIsHere; + private bool _travelingMerchantIsVisited; + private ClickableTextureComponent _travelingMerchantIcon; + + private bool Enabled { get; set; } + private bool HideWhenVisited { get; set; } + + private readonly IModHelper _helper; + #endregion + + + #region Lifecycle + public ShowTravelingMerchant(IModHelper helper) + { + _helper = helper; + } + + public void Dispose() + { + ToggleOption(false); + } + + public void ToggleOption(bool showTravelingMerchant) + { + Enabled = showTravelingMerchant; + + _helper.Events.Display.RenderingHud -= OnRenderingHud; + _helper.Events.Display.RenderedHud -= OnRenderedHud; + _helper.Events.GameLoop.DayStarted -= OnDayStarted; + _helper.Events.Display.MenuChanged -= OnMenuChanged; + + if (showTravelingMerchant) + { + UpdateTravelingMerchant(); + _helper.Events.Display.RenderingHud += OnRenderingHud; + _helper.Events.Display.RenderedHud += OnRenderedHud; + _helper.Events.GameLoop.DayStarted += OnDayStarted; + _helper.Events.Display.MenuChanged += OnMenuChanged; + } + } + + public void ToggleHideWhenVisitedOption(bool hideWhenVisited) + { + HideWhenVisited = hideWhenVisited; + ToggleOption(Enabled); + } + #endregion + + + #region Event subscriptions + private void OnDayStarted(object sender, EventArgs e) + { + UpdateTravelingMerchant(); + } + + private void OnMenuChanged(object sender, MenuChangedEventArgs e) + { + if (e.NewMenu is ShopMenu menu && menu.forSale.Any(s => !(s is Hat)) && Game1.currentLocation.Name == "Forest") + { + _travelingMerchantIsVisited = true; + } + } + + private void OnRenderingHud(object sender, RenderingHudEventArgs e) + { + // Draw icon + if (!Game1.eventUp && ShouldDrawIcon()) + { + Point iconPosition = IconHandler.Handler.GetNewIconPosition(); + _travelingMerchantIcon = new ClickableTextureComponent( + new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), + Game1.mouseCursors, + new Rectangle(192, 1411, 20, 20), + 2f + ); + _travelingMerchantIcon.draw(Game1.spriteBatch); + } + } + + private void OnRenderedHud(object sender, RenderedHudEventArgs e) + { + // Show text on hover + if (ShouldDrawIcon() && (_travelingMerchantIcon?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false)) + { + string hoverText = _helper.SafeGetString(LanguageKeys.TravelingMerchantIsInTown); + IClickableMenu.drawHoverText(Game1.spriteBatch, hoverText, Game1.dialogueFont); + } + } + #endregion + + + #region Logic + private void UpdateTravelingMerchant() + { + int dayOfWeek = Game1.dayOfMonth % 7; + _travelingMerchantIsHere = dayOfWeek == 0 || dayOfWeek == 5; + + _travelingMerchantIsVisited = false; + } + + private bool ShouldDrawIcon() { - #region Properties - private bool _travelingMerchantIsHere; - private bool _travelingMerchantIsVisited; - private ClickableTextureComponent _travelingMerchantIcon; - - private bool Enabled { get; set; } - private bool HideWhenVisited { get; set; } - - private readonly IModHelper _helper; - #endregion - - - #region Lifecycle - public ShowTravelingMerchant(IModHelper helper) - { - _helper = helper; - } - - public void Dispose() - { - ToggleOption(false); - } - - public void ToggleOption(bool showTravelingMerchant) - { - Enabled = showTravelingMerchant; - - _helper.Events.Display.RenderingHud -= OnRenderingHud; - _helper.Events.Display.RenderedHud -= OnRenderedHud; - _helper.Events.GameLoop.DayStarted -= OnDayStarted; - _helper.Events.Display.MenuChanged -= OnMenuChanged; - - if (showTravelingMerchant) - { - UpdateTravelingMerchant(); - _helper.Events.Display.RenderingHud += OnRenderingHud; - _helper.Events.Display.RenderedHud += OnRenderedHud; - _helper.Events.GameLoop.DayStarted += OnDayStarted; - _helper.Events.Display.MenuChanged += OnMenuChanged; - } - } - - public void ToggleHideWhenVisitedOption(bool hideWhenVisited) - { - HideWhenVisited = hideWhenVisited; - ToggleOption(Enabled); - } - #endregion - - - #region Event subscriptions - private void OnDayStarted(object sender, EventArgs e) - { - UpdateTravelingMerchant(); - } - - private void OnMenuChanged(object sender, MenuChangedEventArgs e) - { - if (e.NewMenu is ShopMenu menu && menu.forSale.Any(s => !(s is Hat)) && Game1.currentLocation.Name == "Forest") - { - _travelingMerchantIsVisited = true; - } - } - - private void OnRenderingHud(object sender, RenderingHudEventArgs e) - { - // Draw icon - if (!Game1.eventUp && ShouldDrawIcon()) - { - Point iconPosition = IconHandler.Handler.GetNewIconPosition(); - _travelingMerchantIcon = - new ClickableTextureComponent( - new Rectangle(iconPosition.X, iconPosition.Y, 40, 40), - Game1.mouseCursors, - new Rectangle(192, 1411, 20, 20), - 2f); - _travelingMerchantIcon.draw(Game1.spriteBatch); - } - } - - private void OnRenderedHud(object sender, RenderedHudEventArgs e) - { - // Show text on hover - if (ShouldDrawIcon() && (_travelingMerchantIcon?.containsPoint(Game1.getMouseX(), Game1.getMouseY()) ?? false)) - { - string hoverText = _helper.SafeGetString(LanguageKeys.TravelingMerchantIsInTown); - IClickableMenu.drawHoverText( - Game1.spriteBatch, - hoverText, - Game1.dialogueFont - ); - } - } - #endregion - - - #region Logic - private void UpdateTravelingMerchant() - { - int dayOfWeek = Game1.dayOfMonth % 7; - _travelingMerchantIsHere = dayOfWeek == 0 || dayOfWeek == 5; - - _travelingMerchantIsVisited = false; - } - - private bool ShouldDrawIcon() - { - return _travelingMerchantIsHere && (!_travelingMerchantIsVisited || !HideWhenVisited); - } - #endregion + return _travelingMerchantIsHere && (!_travelingMerchantIsVisited || !HideWhenVisited); } + #endregion + } } diff --git a/UIInfoSuite2/UIElements/ShowWhenAnimalNeedsPet.cs b/UIInfoSuite2/UIElements/ShowWhenAnimalNeedsPet.cs index db692cb6..ff82b5d4 100644 --- a/UIInfoSuite2/UIElements/ShowWhenAnimalNeedsPet.cs +++ b/UIInfoSuite2/UIElements/ShowWhenAnimalNeedsPet.cs @@ -1,4 +1,7 @@ -using Microsoft.Xna.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Netcode; using StardewModdingAPI; @@ -6,223 +9,258 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Characters; +using StardewValley.GameData.FarmAnimals; +using StardewValley.ItemTypeDefinitions; using StardewValley.Locations; using StardewValley.Network; -using System; -using System.Linq; namespace UIInfoSuite2.UIElements { - internal class ShowWhenAnimalNeedsPet : IDisposable - { - #region Properties - private readonly PerScreen _yMovementPerDraw = new(); - private readonly PerScreen _alpha = new(); + internal class ShowWhenAnimalNeedsPet : IDisposable + { + #region Properties + private readonly PerScreen _yMovementPerDraw = new(); + private readonly PerScreen _alpha = new(); - private bool Enabled { get; set; } - private bool HideOnMaxFriendship { get; set; } + private bool Enabled { get; set; } + private bool HideOnMaxFriendship { get; set; } - private readonly IModHelper _helper; - #endregion + private readonly IModHelper _helper; + #endregion - #region Lifecycle - public ShowWhenAnimalNeedsPet(IModHelper helper) - { - _helper = helper; - } + #region Lifecycle + public ShowWhenAnimalNeedsPet(IModHelper helper) + { + _helper = helper; + } - public void Dispose() - { - ToggleOption(false); - } + public void Dispose() + { + ToggleOption(false); + } - public void ToggleOption(bool showWhenAnimalNeedsPet) - { - Enabled = showWhenAnimalNeedsPet; + public void ToggleOption(bool showWhenAnimalNeedsPet) + { + Enabled = showWhenAnimalNeedsPet; - _helper.Events.Player.Warped -= OnWarped; - _helper.Events.Display.RenderingHud -= OnRenderingHud_DrawAnimalHasProduct; - _helper.Events.Display.RenderingHud -= OnRenderingHud_DrawNeedsPetTooltip; - _helper.Events.GameLoop.UpdateTicked -= UpdateTicked; + _helper.Events.Player.Warped -= OnWarped; + _helper.Events.Display.RenderingHud -= OnRenderingHud_DrawAnimalHasProduct; + _helper.Events.Display.RenderingHud -= OnRenderingHud_DrawNeedsPetTooltip; + _helper.Events.GameLoop.UpdateTicked -= UpdateTicked; - if (showWhenAnimalNeedsPet) - { - _helper.Events.Player.Warped += OnWarped; - _helper.Events.Display.RenderingHud += OnRenderingHud_DrawAnimalHasProduct; - _helper.Events.Display.RenderingHud += OnRenderingHud_DrawNeedsPetTooltip; - _helper.Events.GameLoop.UpdateTicked += UpdateTicked; - } - } - public void ToggleDisableOnMaxFriendshipOption(bool hideOnMaxFriendship) - { - HideOnMaxFriendship = hideOnMaxFriendship; - ToggleOption(Enabled); - } - #endregion + if (showWhenAnimalNeedsPet) + { + _helper.Events.Player.Warped += OnWarped; + _helper.Events.Display.RenderingHud += OnRenderingHud_DrawAnimalHasProduct; + _helper.Events.Display.RenderingHud += OnRenderingHud_DrawNeedsPetTooltip; + _helper.Events.GameLoop.UpdateTicked += UpdateTicked; + } + } + public void ToggleDisableOnMaxFriendshipOption(bool hideOnMaxFriendship) + { + HideOnMaxFriendship = hideOnMaxFriendship; + ToggleOption(Enabled); + } + #endregion - #region Event subscriptions - private void OnWarped(object sender, WarpedEventArgs e) - { - } + #region Event subscriptions + private void OnWarped(object sender, WarpedEventArgs e) { } - private void OnRenderingHud_DrawNeedsPetTooltip(object sender, RenderingHudEventArgs e) - { - if (!Game1.eventUp && Game1.activeClickableMenu == null && (Game1.currentLocation is AnimalHouse || Game1.currentLocation is Farm || Game1.currentLocation is FarmHouse)) - { - DrawIconForFarmAnimals(); - DrawIconForPets(); - } - } + private void OnRenderingHud_DrawNeedsPetTooltip(object sender, RenderingHudEventArgs e) + { + if (!Game1.eventUp && + Game1.activeClickableMenu == null && + (Game1.currentLocation is AnimalHouse || Game1.currentLocation is Farm || Game1.currentLocation is FarmHouse)) + { + DrawIconForFarmAnimals(); + DrawIconForPets(); + } + } - private void OnRenderingHud_DrawAnimalHasProduct(object sender, RenderingHudEventArgs e) - { - if (!Game1.eventUp && Game1.activeClickableMenu == null && (Game1.currentLocation is AnimalHouse || Game1.currentLocation is Farm)) - { - DrawAnimalHasProduct(); - } - } + private void OnRenderingHud_DrawAnimalHasProduct(object sender, RenderingHudEventArgs e) + { + if (!Game1.eventUp && + Game1.activeClickableMenu == null && + (Game1.currentLocation is AnimalHouse || Game1.currentLocation is Farm)) + { + DrawAnimalHasProduct(); + } + } - private void UpdateTicked(object sender, UpdateTickedEventArgs e) - { - if (Game1.eventUp || Game1.activeClickableMenu != null || !(Game1.currentLocation is AnimalHouse || Game1.currentLocation is Farm || Game1.currentLocation is FarmHouse)) - return; + private void UpdateTicked(object sender, UpdateTickedEventArgs e) + { + if (Game1.eventUp || + Game1.activeClickableMenu != null || + !(Game1.currentLocation is AnimalHouse || + Game1.currentLocation is Farm || + Game1.currentLocation is FarmHouse)) + { + return; + } - float sine = (float)Math.Sin(e.Ticks / 20.0); - _yMovementPerDraw.Value = -6f + 6f * sine; - _alpha.Value = 0.8f + 0.2f * sine; - } - #endregion + var sine = (float)Math.Sin(e.Ticks / 20.0); + _yMovementPerDraw.Value = -6f + 6f * sine; + _alpha.Value = 0.8f + 0.2f * sine; + } + #endregion - #region Logic - private void DrawAnimalHasProduct() + #region Logic + private void DrawAnimalHasProduct() + { + NetLongDictionary>? animalsInCurrentLocation = GetAnimalsInCurrentLocation(); + if (animalsInCurrentLocation != null) + { + foreach (KeyValuePair animal in animalsInCurrentLocation.Pairs) { - var animalsInCurrentLocation = GetAnimalsInCurrentLocation(); - if (animalsInCurrentLocation != null) - { - foreach (var animal in animalsInCurrentLocation.Pairs) - { - if (animal.Value.harvestType.Value != FarmAnimal.layHarvestType && - !animal.Value.IsEmoting && - animal.Value.currentProduce.Value != 430 && - animal.Value.currentProduce.Value > 0 && - animal.Value.age.Value >= animal.Value.ageWhenMature.Value) - { - Vector2 positionAboveAnimal = GetPetPositionAboveAnimal(animal.Value); - positionAboveAnimal.Y += (float)(Math.Sin(Game1.currentGameTime.TotalGameTime.TotalMilliseconds / 300.0 + animal.Value.Name.GetHashCode()) * 5.0); - Game1.spriteBatch.Draw( - Game1.emoteSpriteSheet, - Utility.ModifyCoordinatesForUIScale(new Vector2(positionAboveAnimal.X + 14f, positionAboveAnimal.Y)), - new Rectangle(3 * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, 3 * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4), - Color.White * 0.9f, - 0.0f, - Vector2.Zero, - 4f, - SpriteEffects.None, - 1f); - - Rectangle sourceRectangle = GameLocation.getSourceRectForObject(animal.Value.currentProduce.Value); - Game1.spriteBatch.Draw( - Game1.objectSpriteSheet, - Utility.ModifyCoordinatesForUIScale(new Vector2(positionAboveAnimal.X + 28f, positionAboveAnimal.Y + 8f)), - sourceRectangle, - Color.White * 0.9f, - 0.0f, - Vector2.Zero, - 2.2f, - SpriteEffects.None, - 1f); - } - } - } + FarmAnimalHarvestType? harvestType = animal.Value.GetHarvestType(); + if (harvestType != FarmAnimalHarvestType.DropOvernight && + !animal.Value.IsEmoting && + animal.Value.currentProduce.Value != "430" && // 430 is truffle + animal.Value.currentProduce.Value != null && + animal.Value.age.Value >= animal.Value.GetAnimalData().DaysToMature) + { + Vector2 positionAboveAnimal = GetPetPositionAboveAnimal(animal.Value); + positionAboveAnimal.Y += (float)(Math.Sin( + Game1.currentGameTime.TotalGameTime.TotalMilliseconds / 300.0 + + animal.Value.Name.GetHashCode() + ) * + 5.0); + Game1.spriteBatch.Draw( + Game1.emoteSpriteSheet, + Utility.ModifyCoordinatesForUIScale(new Vector2(positionAboveAnimal.X + 14f, positionAboveAnimal.Y)), + new Rectangle( + 3 * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, + 3 * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), + Game1.tileSize / 4, + Game1.tileSize / 4 + ), + Color.White * 0.9f, + 0.0f, + Vector2.Zero, + 4f, + SpriteEffects.None, + 1f + ); + + string produceItemId = animal.Value.currentProduce.Value; + ParsedItemData? produceData = ItemRegistry.GetData(produceItemId); + Rectangle sourceRectangle = produceData.GetSourceRect(); + Game1.spriteBatch.Draw( + produceData.GetTexture(), + Utility.ModifyCoordinatesForUIScale(new Vector2(positionAboveAnimal.X + 28f, positionAboveAnimal.Y + 8f)), + sourceRectangle, + Color.White * 0.9f, + 0.0f, + Vector2.Zero, + 2.2f, + SpriteEffects.None, + 1f + ); + } } + } + } - private void DrawIconForFarmAnimals() + private void DrawIconForFarmAnimals() + { + NetLongDictionary>? animalsInCurrentLocation = GetAnimalsInCurrentLocation(); + + if (animalsInCurrentLocation != null) + { + foreach (KeyValuePair animal in animalsInCurrentLocation.Pairs) { - var animalsInCurrentLocation = GetAnimalsInCurrentLocation(); + if (!animal.Value.IsEmoting && + !animal.Value.wasPet.Value && + (animal.Value.friendshipTowardFarmer.Value < 1000 || !HideOnMaxFriendship)) + { + Vector2 positionAboveAnimal = GetPetPositionAboveAnimal(animal.Value); + string animalType = animal.Value.type.Value.ToLower(); - if (animalsInCurrentLocation != null) + if (animalType.Contains("cow") || + animalType.Contains("sheep") || + animalType.Contains("goat") || + animalType.Contains("pig")) { - foreach (var animal in animalsInCurrentLocation.Pairs) - { - if (!animal.Value.IsEmoting && - !animal.Value.wasPet.Value && - (animal.Value.friendshipTowardFarmer.Value < 1000 || !HideOnMaxFriendship)) - { - Vector2 positionAboveAnimal = GetPetPositionAboveAnimal(animal.Value); - string animalType = animal.Value.type.Value.ToLower(); - - if (animalType.Contains("cow") || - animalType.Contains("sheep") || - animalType.Contains("goat") || - animalType.Contains("pig")) - { - positionAboveAnimal.X += 50f; - positionAboveAnimal.Y += 50f; - } - Game1.spriteBatch.Draw( - Game1.mouseCursors, - Utility.ModifyCoordinatesForUIScale(new Vector2(positionAboveAnimal.X, positionAboveAnimal.Y + _yMovementPerDraw.Value)), - new Rectangle(32, 0, 16, 16), - Color.White * _alpha.Value, - 0.0f, - Vector2.Zero, - 4f, - SpriteEffects.None, - 1f); - } - } + positionAboveAnimal.X += 50f; + positionAboveAnimal.Y += 50f; } - } - private void DrawIconForPets() - { - foreach (var character in Game1.currentLocation.characters) - { - if (character is Pet pet && - !pet.lastPetDay.Values.Any(day => day == Game1.Date.TotalDays) - && (pet.friendshipTowardFarmer.Value < 1000 || !HideOnMaxFriendship)) - { - Vector2 positionAboveAnimal = GetPetPositionAboveAnimal(character); - positionAboveAnimal.X += 50f; - positionAboveAnimal.Y -= 20f; - Game1.spriteBatch.Draw( - Game1.mouseCursors, - Utility.ModifyCoordinatesForUIScale(new Vector2(positionAboveAnimal.X, positionAboveAnimal.Y + _yMovementPerDraw.Value)), - new Rectangle(32, 0, 16, 16), - Color.White * _alpha.Value, - 0.0f, - Vector2.Zero, - 4f, - SpriteEffects.None, - 1f); - } - } + Game1.spriteBatch.Draw( + Game1.mouseCursors, + Utility.ModifyCoordinatesForUIScale( + new Vector2(positionAboveAnimal.X, positionAboveAnimal.Y + _yMovementPerDraw.Value) + ), + new Rectangle(32, 0, 16, 16), + Color.White * _alpha.Value, + 0.0f, + Vector2.Zero, + 4f, + SpriteEffects.None, + 1f + ); + } } + } + } - private Vector2 GetPetPositionAboveAnimal(Character animal) + private void DrawIconForPets() + { + foreach (NPC? character in Game1.currentLocation.characters) + { + if (character is Pet pet && + !pet.lastPetDay.Values.Any(day => day == Game1.Date.TotalDays) && + (pet.friendshipTowardFarmer.Value < 1000 || !HideOnMaxFriendship)) { - return new Vector2(Game1.viewport.Width <= Game1.currentLocation.map.DisplayWidth ? animal.position.X - Game1.viewport.X + 16 : animal.position.X + ((Game1.viewport.Width - Game1.currentLocation.map.DisplayWidth) / 2 + 18), - Game1.viewport.Height <= Game1.currentLocation.map.DisplayHeight ? animal.position.Y - Game1.viewport.Y - 34 : animal.position.Y + ((Game1.viewport.Height - Game1.currentLocation.map.DisplayHeight) / 2 - 50)); + Vector2 positionAboveAnimal = GetPetPositionAboveAnimal(character); + positionAboveAnimal.X += 50f; + positionAboveAnimal.Y -= 20f; + Game1.spriteBatch.Draw( + Game1.mouseCursors, + Utility.ModifyCoordinatesForUIScale( + new Vector2(positionAboveAnimal.X, positionAboveAnimal.Y + _yMovementPerDraw.Value) + ), + new Rectangle(32, 0, 16, 16), + Color.White * _alpha.Value, + 0.0f, + Vector2.Zero, + 4f, + SpriteEffects.None, + 1f + ); } + } + } - private NetLongDictionary> GetAnimalsInCurrentLocation() - { - NetLongDictionary> animals = null; + private Vector2 GetPetPositionAboveAnimal(Character animal) + { + return new Vector2( + Game1.viewport.Width <= Game1.currentLocation.map.DisplayWidth + ? animal.position.X - Game1.viewport.X + 16 + : animal.position.X + ((Game1.viewport.Width - Game1.currentLocation.map.DisplayWidth) / 2 + 18), + Game1.viewport.Height <= Game1.currentLocation.map.DisplayHeight + ? animal.position.Y - Game1.viewport.Y - 34 + : animal.position.Y + ((Game1.viewport.Height - Game1.currentLocation.map.DisplayHeight) / 2 - 50) + ); + } - if (Game1.currentLocation is AnimalHouse) - { - animals = (Game1.currentLocation as AnimalHouse).animals; - } - else if (Game1.currentLocation is Farm) - { - animals = (Game1.currentLocation as Farm).animals; - } + private NetLongDictionary> GetAnimalsInCurrentLocation() + { + NetLongDictionary> animals = null; - return animals; - } - #endregion + if (Game1.currentLocation is AnimalHouse) + { + animals = (Game1.currentLocation as AnimalHouse).animals; + } + else if (Game1.currentLocation is Farm) + { + animals = (Game1.currentLocation as Farm).animals; + } + + return animals; } + #endregion + } } diff --git a/UIInfoSuite2/UIInfoSuite2.csproj b/UIInfoSuite2/UIInfoSuite2.csproj index 5c9d06b1..7c59f215 100644 --- a/UIInfoSuite2/UIInfoSuite2.csproj +++ b/UIInfoSuite2/UIInfoSuite2.csproj @@ -1,14 +1,15 @@ - 2.2.10 - net5.0 + 2.3.0-alpha-1 + net6.0 enable + $(SolutionDir)\Releases - + - + diff --git a/UIInfoSuite2/manifest.json b/UIInfoSuite2/manifest.json index 6f8c516a..3b35548d 100644 --- a/UIInfoSuite2/manifest.json +++ b/UIInfoSuite2/manifest.json @@ -1,10 +1,12 @@ { - "Name": "UI Info Suite 2", - "Author": "Annosz", - "Version": "2.2.10", - "Description": "Adds a useful information to the user interface. Based on Cdaragorn's excellent UI Info Suite.", - "UniqueID": "Annosz.UiInfoSuite2", - "EntryDll": "UIInfoSuite2.dll", - "MinimumApiVersion": "3.0.0", - "UpdateKeys": [ "GitHub:Annosz/UIInfoSuite2" ] + "Name": "UI Info Suite 2", + "Author": "Annosz", + "Version": "2.3.0-alpha-1", + "Description": "Adds a useful information to the user interface. Based on Cdaragorn's excellent UI Info Suite.", + "UniqueID": "Annosz.UiInfoSuite2", + "EntryDll": "UIInfoSuite2.dll", + "MinimumApiVersion": "4.0.0", + "UpdateKeys": [ + "GitHub:Annosz/UIInfoSuite2" + ] }