Compare commits

...

367 Commits

Author SHA1 Message Date
e50f0fa976 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-04-16 15:02:57 +02:00
a63b9b50b6 Revert "Merge branch 'master' of https://code.iamtalon.me/talon/stardew-access"
This reverts commit cdb262a26a, reversing
changes made to 6128e2dd72.
2023-04-16 15:01:59 +02:00
Mohammad Shoaib Khan
3c8b78192a Added translations for trash bear wanted item
Added missing translations for heart count entry
2023-04-10 22:37:07 +05:30
Mohammad Shoaib Khan
4ffd9bc9bc Added heart count to animal query menu 2023-04-10 22:37:07 +05:30
Mohammad Shoaib Khan
5f612a20c1 Patched the checkAction method instead to avoid the patch from executing multiple times 2023-04-10 22:37:07 +05:30
Mohammad Shoaib Khan
6c14d040be Patched draw method of TrashBear.cs to speak the wanted item 2023-04-10 22:37:07 +05:30
Mohammad Shoaib Khan
ca249644dd Fixed bug for TextBoxEntryMenu 2023-04-10 15:33:19 +05:30
Mohammad Shoaib Khan
be8b9a9e41 Bug which prevented from moving or purchasing animals fixed 2023-04-10 13:15:43 +05:30
02250ed307 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-04-09 09:27:17 +02:00
Mohammad Shoaib Khan
09001807cb Fixed TileInfo.cs bug -
Added unimplemented code for ladders, shaft and mine elevator
2023-04-08 00:13:13 +05:30
cdb262a26a Merge branch 'master' of https://code.iamtalon.me/talon/stardew-access 2023-04-07 09:24:07 +02:00
6128e2dd72 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-04-07 09:09:26 +02:00
Mohammad Shoaib Khan
0e1f9d0cab Merge pull request #96 from conundrum9999/main
Text descriptions for character creation; speed optimizations for object search; load NVDA's dll in place.
2023-04-07 12:00:47 +05:30
Katie Durden
803bc64868 Bump manifest version to 1.3.5-beta2. Fix error in json loading and missing static tiles. 2023-04-06 15:02:08 -07:00
Katie Durden
dacbff92ea CharacterCustomizationMenuPatches now reads descriptions of appearance options (thanks @JJpanSai for writing the descriptions!)
Descriptions are stored in assets/new-character-appearance-descriptions.json.
If file can't be found or is corrupted, fallback on old behavior of just reading numbers.
Functions support a `lessInfo` variable that also turns off the descriptions, though the config option is not yet implemented.
2023-04-05 19:43:36 -07:00
Katie Durden
529a110617 Refactor functions that obtain current attribute values to share code. 2023-04-05 19:41:47 -07:00
Katie Durden
ed405a139e Remove dependency on Newtonsoft.Json since we're now relying on System.Text.Json.
Some optimization and refactoring in ModEntry.cs
Other minor fixes
2023-04-05 18:37:15 -07:00
Katie Durden
ad0cb687b7 Readded lessInfo to many tiles related functions and other code cleanup (making argument ordering consist. 2023-04-05 18:37:15 -07:00
Katie Durden
91643e184f Finish adding documentation (with Chat GPT's help) and refactoring TileInfo.cs.
Reduced code duplication with helper functions where possible.
Optimized and simplified `getNameWithCategoryAtTile`.
2023-04-05 18:37:15 -07:00
Katie Durden
8509cbfc0b Refactored getDynamicTilesInfo from TileInfo.cs by moving most of it's functionality to new file DynamicTiles.cs.
Refactored handling of dynamic tiles for events; tile coords now loaded from json file.
Moved LoadJsonFile function to utils.cs.
2023-04-05 18:37:15 -07:00
Katie Durden
35115223d3 Update .gitattributes to ensure .cs and .json files keep crlf line endings. 2023-04-05 18:37:15 -07:00
Katie Durden
a7dfeb5526 Optimization and refactoring of TileInfo.cs
Reduced number of calls to `isCollidingAtTile` as it calls a StardewValley function that takes ~30 ms to complete.
Optimize `getFarmAnimalAt` in `TileInfo`.
Refactor `getResourceClumpAtTile` and `getStumpsInWoods`.
2023-04-05 18:37:15 -07:00
Katie Durden
34c182ddc6 Cache the value of Game1.currentLocation 2023-04-05 18:37:15 -07:00
Katie Durden
245959366c Refactor StaticTiles and CATEGORY classes for enhanced functionality
`StaticTiles` now uses `System.Text.Json` and supports conditional and mod-based maps with efficient dictionary lookups. Dictionary can be reloaded as needed, typically once per game day.
`CATEGORY` allows runtime addition of new categories, with an indexer function for retrieval, and retains static properties for compiled categories.
2023-04-05 18:37:15 -07:00
Katie Durden
5d8f85fa5b Separated tile file loading from StaticTiles constructor 2023-04-05 18:37:15 -07:00
Katie Durden
dd812851b4 Add base for improved tile coordinate lookup functionality; typo fixes.
* Build tile dictionary at game launch to speed up tile lookups during gameplay.
* Add GameLaunched event to setup new dictionary after other mods loaded.
* Change `StaticTiles.getStaticTileInfoAtWithCategory` to use new dictionary for lookup.
* Various typo fixes and other code cleanup
2023-04-05 18:37:15 -07:00
Katie Durden
4b2e31fadc Change comparisons relying on string.ToLower() to use StringComparison.OrdinalIgnoreCase. 2023-04-05 18:37:15 -07:00
Katie Durden
541fd42133 Static tiles uses HashSet to lookup entries. More nullchecks changed to use is. 2023-04-05 18:37:15 -07:00
Katie Durden
a88172c0cf Replaced some foreach loops with for loops and nullchecks to use is instead of ==. 2023-04-05 18:37:15 -07:00
Katie Durden
e9ba7c96b9 Changes searched from List to HashSet; saves ~43ms in `SearchLocation 2023-04-05 18:37:15 -07:00
Katie Durden
32db14b329 add <modpath>/libraries/windows to the dll search path before loading Tolk. This enables nvdaControllerClient64.dll to be loaded in place. 2023-04-05 18:37:15 -07:00
Katie Durden
1c61580d6b Fix inventory not speaking extra info; not respecting config option. Reduce excess commas. Bump version to 1.3.5-alpha2. 2023-04-05 18:37:15 -07:00
Mohammad Shoaib Khan
7bb4819601 Merge pull request #94 from khanshoaib3/Issue90
Speak year and season on day 1
2023-04-04 16:01:55 +05:30
Mohammad Shoaib Khan
ce6f67a0dc Speak year and season on day 1 2023-04-04 15:59:40 +05:30
c3cec1a490 Merge remote-tracking branch 'github/fishing' 2023-03-26 23:24:52 +02:00
8cca880b04 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-03-10 15:59:58 +01:00
Mohammad Shoaib Khan
5fa9fd5eca Merge branch 'master' into fishing
Conflicts:
	stardew-access/CustomSoundEffects.cs
2023-03-10 15:44:12 +05:30
Mohammad Shoaib Khan
b24fe44dd3 Merge pull request #91 from khanshoaib3/refactor
Patches Refactor
2023-03-10 15:37:09 +05:30
Mohammad Shoaib Khan
2b6b60494e Grouped files 2023-03-10 15:33:39 +05:30
Mohammad Shoaib Khan
b8fce68bec Moved more menus to their own class 2023-03-10 15:24:08 +05:30
Mohammad Shoaib Khan
ea03f9a4cb Moved some menus to their own class 2023-03-10 15:09:56 +05:30
Mohammad Shoaib Khan
b0c1d9e173 Moved tailoring menu patch to its own class 2023-03-10 15:00:31 +05:30
Mohammad Shoaib Khan
af0ec1ab8d Moved pond query menu patch to its own class 2023-03-10 14:55:07 +05:30
Mohammad Shoaib Khan
07fcc94296 Moved forge menu patch to its own class 2023-03-10 14:50:40 +05:30
Mohammad Shoaib Khan
64300c58eb Moved item list menu patch to its own class 2023-03-10 14:41:30 +05:30
Mohammad Shoaib Khan
a250cbd98d Renamed classes 2023-03-10 14:35:46 +05:30
Mohammad Shoaib Khan
c553b589ab Moved patches related to Game1 and InstanceGame to their own class 2023-03-10 14:32:43 +05:30
Mohammad Shoaib Khan
50e8afc78c Moved and organized quest patches 2023-03-09 14:01:58 +05:30
Mohammad Shoaib Khan
53fe731401 Moved and organized mini game patches 2023-03-09 13:32:45 +05:30
Mohammad Shoaib Khan
2c233f6e6b Moved InventoryUtils.cs 2023-03-09 13:27:02 +05:30
Mohammad Shoaib Khan
1346e90bf5 Organised code for geode menu patch 2023-03-09 13:21:46 +05:30
Mohammad Shoaib Khan
8970a2c441 Fixed bug for Museum menu patch 2023-03-09 13:12:07 +05:30
Mohammad Shoaib Khan
2ddebd3615 Organised code for donation menu patches 2023-03-09 13:06:13 +05:30
Mohammad Shoaib Khan
5195313a5b Organised code for item grab menu patch 2023-03-08 12:44:07 +05:30
Mohammad Shoaib Khan
6822bd4840 Organised code for purchase animal menu patch 2023-03-08 12:29:19 +05:30
Mohammad Shoaib Khan
80963c4dc6 Improved and organised code in shop menu patch 2023-03-08 12:21:50 +05:30
Mohammad Shoaib Khan
8523026ed0 Improved and organised code for dialogue box patch
Moved letter viewer menu patch to its own class
moved npc patch to its own clas
moved draw hover text patch to IClickableMenuPatch.cs
2023-03-08 12:06:46 +05:30
Mohammad Shoaib Khan
2ba41822c9 Improved and organised code in title menu patches 2023-03-07 23:34:07 +05:30
Mohammad Shoaib Khan
9096ce59b9 bug fix in snapping mouse to primary and secondary inventory slots 2023-03-07 22:57:15 +05:30
Mohammad Shoaib Khan
6141447c13 Improved and organised code in carpenter menu patch 2023-03-07 22:45:00 +05:30
Mohammad Shoaib Khan
baede57965 Improved and organised code in bundle menu patches 2023-03-07 19:44:58 +05:30
Mohammad Shoaib Khan
b0ef71cd4a bug fix 2023-03-07 12:16:39 +05:30
Mohammad Shoaib Khan
403a97b633 Improved and organised code in AnimalQueryMenuPatch.cs 2023-03-07 12:03:08 +05:30
Mohammad Shoaib Khan
b22af63b97 Improved and organised code in SocialPagePatch.cs 2023-03-07 11:29:23 +05:30
Mohammad Shoaib Khan
774454ce0e Refactored code CraftingPagePatch.cs 2023-03-03 21:03:37 +05:30
Mohammad Shoaib Khan
0fa90e4c74 Improved and organised code in InventoryPagePatch.cs 2023-02-24 20:45:35 +05:30
Mohammad Shoaib Khan
15380e5b76 Improved game menu patch code
- Added active suffix if the hovered tab button matches the current tab
2023-02-24 20:44:54 +05:30
Mohammad Shoaib Khan
911c93a555 Moved game menu patch 2023-02-24 19:45:01 +05:30
Mohammad Shoaib Khan
8d11cc6266 Moved exit page patch 2023-02-24 19:40:01 +05:30
Mohammad Shoaib Khan
820d714594 Moved options page patch 2023-02-24 19:35:58 +05:30
Mohammad Shoaib Khan
7c1e600790 Moved inventory page patch 2023-02-24 19:33:14 +05:30
Mohammad Shoaib Khan
f851806947 Moved crafting page patch 2023-02-24 14:43:19 +05:30
Mohammad Shoaib Khan
9d7c2c36ec Moved social page patch 2023-02-24 14:37:21 +05:30
Mohammad Shoaib Khan
2a5baa6770 Moved collections page patch 2023-02-24 14:33:03 +05:30
Mohammad Shoaib Khan
6730fcceff Moved geode menu patch 2023-02-24 14:29:51 +05:30
Mohammad Shoaib Khan
7d505d2dd7 Moved item grab menu patch 2023-02-24 14:10:28 +05:30
Mohammad Shoaib Khan
4a39bf94ea Moved shop menu patch 2023-02-24 13:49:50 +05:30
Mohammad Shoaib Khan
6d3962ea4d Refactored code BuildingNAnimalMenuPatches.cs 2023-02-23 21:41:05 +05:30
1a8b971a91 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-02-22 16:49:04 +01:00
Mohammad Shoaib
d12ecf0fae Merge pull request #88 from khanshoaib3/Issue79
TextBox QOL update
2023-02-22 18:11:39 +05:30
Mohammad Shoaib Khan
ac190f5939 Bug fix 2023-02-22 18:09:43 +05:30
Mohammad Shoaib Khan
8b0350497b Refactored code 2023-02-22 17:59:11 +05:30
Mohammad Shoaib Khan
8059e09089 Used a different method to detect if any text box is active or not
This fixes the issue when more than one text boxes are present in a menu
2023-02-22 17:45:59 +05:30
Mohammad Shoaib Khan
cf0e46ecdd Refactored code 2023-02-22 17:03:33 +05:30
Mohammad Shoaib Khan
31f3144bfd Bug fix 2023-02-22 16:55:54 +05:30
Mohammad Shoaib Khan
ecb256ad57 Removed unnecessary code 2023-02-21 21:51:55 +05:30
Mohammad Shoaib Khan
c9c62d4fa9 Removed previous text box code from other places 2023-02-21 21:49:22 +05:30
Mohammad Shoaib Khan
e595a03be6 Fixed bug for more than 1 text boxes in a menu
In this bug, if there were amore than 1 text boxes, then the textBoxQuery was resets to ' '
even if a text box is selected because of the other unselected text boxes
2023-02-21 21:42:46 +05:30
Mohammad Shoaib Khan
5296c4cabe Added patch to disable left mouse sim key when a tex box is active
Also speak the content of the text box when active
2023-02-21 21:28:38 +05:30
b6e8129b78 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-02-21 09:18:46 +01:00
Mohammad Shoaib
07bd1bf1ef Merge pull request #87 from khanshoaib3/Issue59
Inventory sound cue and verbosity
2023-02-20 13:55:51 +05:30
Mohammad Shoaib Khan
0a244649ee Removed unnecessary comment 2023-02-20 13:54:39 +05:30
Mohammad Shoaib Khan
b8f77384b2 Added config to disable inventory verbosity
If set to true, disables speaking of 'not usable here' and 'donatable' in inventories
2023-02-20 13:46:06 +05:30
Mohammad Shoaib Khan
2d061e220b Organized code 2023-02-20 13:35:17 +05:30
Mohammad Shoaib Khan
80637af471 Moved narrateHoveredItemInInventory to separate class 2023-02-20 12:59:55 +05:30
Mohammad Shoaib Khan
042afa4d90 Organized code 2023-02-20 12:43:25 +05:30
Mohammad Shoaib Khan
d2c34451b3 Added sound cue when the slot item is not usable 2023-02-20 11:48:24 +05:30
a29eb512a8 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-02-19 18:13:23 +01:00
Mohammad Shoaib Khan
34d773204c Expanded the detection of shipping bin in island 2023-02-19 22:35:57 +05:30
Mohammad Shoaib Khan
9af30890bc Added dynamic detection of sink in farm house
Replace the name of kitchen with stove
2023-02-19 22:26:10 +05:30
Mohammad Shoaib Khan
b6b8ad8c8a Entries for town map can be distinguished for joja run
use 'town_joja' as location name in static-tiles.json or custom-tiles.json
2023-02-19 21:50:57 +05:30
Mohammad Shoaib Khan
feff266d7c Possibly also added mod loaded farm layout support to it.
(not tested)
2023-02-19 17:36:57 +05:30
Mohammad Shoaib Khan
434921a668 Added ability to add entries specific to a certain farm type.
The entries with 'farm' name will be applied to all farm types and
if we want to add an entry specific to a farm type, like to beach farm layout,
we can use 'farm_beach' name for this.
2023-02-18 12:36:39 +05:30
Mohammad Shoaib Khan
016eb87f7d Added licenses of tolk and nvda 2023-02-18 11:17:23 +05:30
Mohammad Shoaib Khan
d17974e3f5 Added sound cues when the bobber is above or below the bar
The pitch of the sound cues also increase with the increase in distance of bobber from bar
Also added a progress sound effect
Added config options to fix the maximum difficulty and motion type
2023-02-12 21:16:48 +05:30
Mohammad Shoaib Khan
b89a8f205d Added progress sound when catching a fish 2023-02-12 15:14:46 +05:30
915273bf78 Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-02-11 11:13:21 +01:00
Mohammad Shoaib Khan
871c0b47cb Added offset to bulletin board junimo bundle
this will potentially fix the issue of the tile not being accessible by the path finder
2023-02-11 15:01:23 +05:30
96d9e8c12f Merge branch 'master' of https://github.com/stardew-access/stardew-access 2023-02-11 09:08:48 +01:00
Mohammad Shoaib Khan
36ce36cfe0 Changed the custom-tiles.json not found error message to info message 2023-02-11 13:19:12 +05:30
04555fe363 Update from upstream. 2023-02-10 13:00:19 +01:00
Mohammad Shoaib Khan
7409959c99 Moved character customization patch to separate file 2023-02-10 11:17:50 +05:30
Katie Durden
baedf69d80 Character creation text boxes announce content if set.
Causes text boxes to report their content instead of " text box" when text was already entered.
E.G. after entering a name, "Farmer's Name Text Box" becomes "Farmer's Name: <name>"
2023-02-09 20:50:36 -08:00
Katie Durden
4c336c36ed Enable access to the character appearance controls.
Added accessible buttons to rotate the farmer, change skin tone, hair style, shirt, pants style, and accessory choice.
Added new accessible slider controls to adjust hue, saturation and value for eye, hair and pants color.
    Up/Down to adjust 1%; PageUp/PageDown to adjust by 10%.
Changed spoken modifiable fields (such as current pet, current shirt) to only announce when relevant controls have focus.
    This includes Random Skin Button for appearance controls.
Added toggle key (LeftControl + Space) to show / hide the appearance controls; hidden by default.
Added announcement when entering customization menu to inform of toggle key.
2023-02-09 07:50:54 -08:00
Katie Durden
fcf81004d3 Typo: isCustomizingChrachter should be isCustomizingCharacter. Updated all occurrences. 2023-02-09 07:42:27 -08:00
Katie Durden
5996fa09f2 Update harmony and Stardew.ModConfig; add newtonsoft.json as dependency. 2023-02-09 07:41:32 -08:00
e5a421a4e3 * Swap out TDSR speech server with libspeak.
* Add a SpeechRate parameter in ModConfig.
2023-02-05 12:15:55 +01:00
44f31419b0 Update to upstream 6e5f9365f0
Remove windows screen reader (probably not a good idea to have separate access forks, but this works for now.)
Send rate when initialising speech (TODO: expose this via a config parameter)
2023-01-20 18:33:52 +01:00
4af19aeb7a Merge remote-tracking branch 'sda/master' into update 2023-01-20 14:26:52 +01:00
Mohammad Shoaib Khan
6e5f9365f0 Adding missing entries 2022-11-26 15:43:17 +05:30
Mohammad Shoaib Khan
4751af8fec Fixed sapi 2022-11-26 15:31:31 +05:30
Bartholomue
d66fd0afb0 IslandWestCave1 Definitions
Added definitions to IslandWestCave1 for the lion statue and note crystals so players can resolve that puzzle.Reverted the removal of the skull cavern door in 1.3.4, since that's an interaction tile used to enter the actual dungeon.
2022-11-26 15:29:52 +05:30
Mohammad Shoaib Khan
771ebb07e4 Added exception for warp points when checking if a tile is on map 2022-11-07 20:51:53 +05:30
Mohammad Shoaib Khan
fe03dfb46e Removed exit and entrance entries 2022-11-07 20:27:11 +05:30
Mohammad Shoaib Khan
ec7148e613 Added warp point detection 2022-11-07 20:07:25 +05:30
Mohammad Shoaib Khan
c4ce0a5280 Added config option to toggle detecting dropped items \n Fixed bug 2022-11-07 19:51:35 +05:30
Bartholomue
dd0d052f64 Static Tiles Container Migration
Changed categorization of multiple objects to fit the new container category for ease of access to important items.
2022-10-29 17:57:04 +05:30
Mohammad Shoaib Khan
0f0824a422 Prioritised custom-tiles.json over static-tiles
- Bug fix in LibraryMuseum
2022-10-29 16:52:37 +05:30
Mohammad Shoaib Khan
f5db6eb655 Renamed chest category to container 2022-10-29 14:48:13 +05:30
Mohammad Shoaib Khan
8aac4499fc Added dropped item detection 2022-10-29 14:41:41 +05:30
Bartholomue
c153462706 Desert Camel
Defined the location of the camel in the desert in the NPC category.
2022-10-29 13:25:47 +05:30
Bartholomue
ac2a384d84 Dwarf Shop Etc
Defined the location of the Dwarf shop on Volcano Dungeon 5.
Defined two signs in town as decorations.
Removed the top and bottom tiles of the Joja Mart shop counter so that Accessible Tiles auto pathing won't navigate to the south / north end of the counter, as those tiles will only let you interact with them from the right.
2022-10-29 13:25:47 +05:30
Mohammad Shoaib Khan
e1a19ae0c8 Bug fix 2022-10-29 13:20:11 +05:30
Mohammad Shoaib Khan
b4560bc9e1 Added custom-tiles.json support
- users can add their own tiles in this file just asin static-tiles.json
- the file needs to be placed just where the static-tiles.json is
2022-10-27 20:05:12 +05:30
Mohammad Shoaib Khan
1187ba30e7 Mod Specific Tiles
We can add tiles that only get detected when the specified mod is loaded
Syntax: <location name>||<Mod's unique id>
2022-10-27 19:30:28 +05:30
Mohammad Shoaib Khan
dccbf8b79f Issue#45 Update buildlist on start of day
- also removed calling restrictions from buildlist command,
 it can now be called from any location
2022-10-27 15:14:37 +05:30
Mohammad Shoaib Khan
9d9b5f5c76 Added books and museum pieces in LibraryMuseum 2022-10-27 14:55:38 +05:30
Mohammad Shoaib Khan
1ea07533bb Fixed minor code warnings 2022-10-27 13:12:41 +05:30
Mohammad Shoaib Khan
f2ba3e8793 Implemented Tolk library for screen reader in windows 2022-10-24 12:12:53 +05:30
Mohammad Shoaib Khan
7a4ca89836 Issue#63 Prepend the player's name in tile viewer 2022-10-23 12:12:27 +05:30
Mohammad Shoaib Khan
1cf4637687 Merge branch 'master' into bugs 2022-10-23 12:03:40 +05:30
Mohammad Shoaib Khan
950ab21d61 Issue#56 Other currency narration 2022-10-23 12:00:30 +05:30
Mohammad Shoaib Khan
9d1c6f95dd Issue#52 Patched speech bubbles 2022-10-23 11:14:58 +05:30
Mohammad Shoaib Khan
d131d26ac8 Issue#54 updated names in static-tiles.json according to the issue 2022-10-22 22:23:14 +05:30
Mohammad Shoaib Khan
a9160b4ff6 Issue#54 the names in static tiles now ignore anything between square brackets 2022-10-22 22:19:38 +05:30
Mohammad Shoaib Khan
34cb47bdaa Issue#42 feeding benches in coops and barns are now dynamic 2022-10-22 21:53:10 +05:30
Mohammad Shoaib Khan
8c080e11f0 Change priority order in getObjectAtTile() 2022-10-10 16:14:14 +05:30
Mohammad Shoaib Khan
5205df8400 Replaced ^ with \n when narrating texts 2022-10-10 15:56:35 +05:30
Mohammad Shoaib Khan
b600eda78e Added command to toggle tts 2022-10-09 13:50:35 +05:30
Mohammad Shoaib Khan
452bbdf86c Fixed duplicates in static-tiles.json 2022-10-09 13:38:26 +05:30
Mohammad Shoaib
66b4333d73 Merge pull request #49 from Bartholomue/master
Joja Mart Definitions ETC
2022-10-06 21:56:40 +05:30
Bartholomue
a0336d630f Expanded Some Tile Ranges
Expanded some tile definitions to cover the full breadth of the objects, in cases where they only listed as one tile but actually spanned a few tiles (such as the exit in the secret woods).
2022-09-30 07:31:09 -05:00
Bartholomue
ad22982b09 Recategorize Pierre's Bin
It was erroneously in the interactable category. It's now been moved to other in order to match the other special orders drop box definitions.
2022-09-26 13:11:52 -05:00
Bartholomue
542d5390a2 Pool And Saloon Definitions
Tile definitions for bathhouse Pool and Saloon, including pool entry steps, decoration, and furniture.
2022-09-18 13:17:34 -05:00
Bartholomue
e1614917a2 Alphabetize Feeding Benches
Renamed the feeding benches to have an alphabet suffix, rather than number. The name sort is by string rather than integer value. Renaming to "Feeding Bench "J" (for example) makes sure that the tenth bench actually comes after the ninth bench, rather than after the first.
2022-09-09 09:24:00 -05:00
Bartholomue
01ec8209af Added Big Shed Redundancy
Duplicated "shed2" to "big shed" for stardew redundancy in name changes upon logging back in to the game.
2022-09-07 06:54:21 -05:00
Bartholomue
0edd6e54e0 Redact static-tiles.json
Re-added the redundancy entries for big barn/coop and deluxe barn/coop to cover Stardew inconsistent behavior.
Removed the static tile definitions for the lost books in preparation for them being made dynamic.
Recommend adding the lost books to the "Other" category so that they'll be easily navigable without cluttering up the other categories which contain important objects.
2022-09-07 03:23:41 -05:00
Bartholomue
2cebc7ce99 More Tile Definitions
Started adding [number] to tile definitions with the same name to coincide with the unique bracketed identifiers.

Adventure Guild:
Defined various decoration and furniture, though I did leave out all the barrels as I felt they weren't important enough to enumerate.

Archaeology House:
Defined the 21 lost books in Strings/Notes under the "Other" category, as their state requires previous interaction to determine.
Defined other various other decoration and furniture.

Town:
Defined the 5X4 fountain in the water category.
Expanded the mountain entrance definition to cover the entire 5X1 tile width.
Defined various benches as furniture.
Defined two bridges in the bridge category.
2022-08-26 07:59:41 -05:00
Mohammad Shoaib Khan
589ec8eff8 Bug fix 2022-08-23 08:58:50 +05:30
Mohammad Shoaib Khan
52c8b6e3e3 Reverted back to using escape key 2022-08-23 08:57:34 +05:30
Bartholomue
a3df4e2591 Joja Mart Definitions
Some more static tile tweaks and additions:
Desert: Added a bench as furniture, helpful for finding a secret note dig spot.
Joja Mart: Added tile definitions for product shelves as decorations.
Railroad: Added a boulder as a decoration, helpful for finding a secret note dig spot.
Railroad: Added the summit boulder as a decoration.
Railroad: Fixed the size of the pond next to the spa to 3X3 rather than 4X3.
Railroad: Added a bench as furniture.
Railroad: Renamed the empty crate tile as "empty box" to match its examine description.
Railroad: Expanded the dumpster definition to span across two tiles, as it's sitting half way across both.
2022-08-22 16:32:42 -05:00
Bartholomue
1bac13bc14 Minor Static Tiles Additions
Added a boulder to the Railroad as a decoration, helpful for finding a secret note dig spot.
Added a 2X1 bench in the desert as furniture, helpful for finding a secret note dig spot.
Fixed the size of the pond next to the spa to 3X3 rather than 4X3.
2022-08-22 06:56:57 -05:00
Mohammad Shoaib Khan
9afeb7f778 Release 1.3.2 2022-08-20 23:28:36 +05:30
Mohammad Shoaib Khan
bf1b904f2f Merge branch 'bugs' 2022-08-20 23:13:41 +05:30
Mohammad Shoaib Khan
c44771bca8 Merge branch 'master' into bugs 2022-08-20 23:09:55 +05:30
Mohammad Shoaib Khan
d4c5bb3e48 Merge branch 'master' into enhancements 2022-08-20 23:09:39 +05:30
Mohammad Shoaib Khan
9d2dd97607 Added 'not usable here' to inventories 2022-08-20 23:08:12 +05:30
Mohammad Shoaib Khan
3083ebca84 Added command to toggle warnings feature 2022-08-20 23:08:12 +05:30
Mohammad Shoaib Khan
895f329b8b Added watered/unwatered toggle for crops 2022-08-20 23:08:12 +05:30
Mohammad Shoaib Khan
741e6a5219 Added 'not usable here' to inventories 2022-08-20 22:58:42 +05:30
Mohammad Shoaib
928b481319 Merge pull request #44 from Bartholomue/master
Updated Static Tiles
2022-08-20 22:21:36 +05:30
Mohammad Shoaib
17c1c19e90 Removed trailing commas 2022-08-20 22:17:45 +05:30
Bartholomue
6f1ccdb951 Updated Static Tiles
Updated tile definitions for multiple locations, recategorizing some for easier object selection without clutter.
2022-08-19 10:05:23 -05:00
Mohammad Shoaib Khan
d14af408df Added command to toggle warnings feature 2022-08-18 12:37:32 +05:30
Mohammad Shoaib Khan
6d18849f17 Added watered/unwatered toggle for crops 2022-08-18 12:18:40 +05:30
Mohammad Shoaib Khan
2df4cfca27 Issue#28 Prepend fish name to fish ponds 2022-08-17 13:28:49 +05:30
Mohammad Shoaib Khan
5bd3e4085d Merge branch 'enhancements'
Merging the new enhancements to master
2022-08-17 13:07:57 +05:30
Mohammad Shoaib Khan
d12e9b2590 Fixed casing issue in static tiles location names and added shafts to read tile 2022-08-17 13:06:47 +05:30
Mohammad Shoaib Khan
a908e09884 Issue#34 Patched upgrades for sprinklers 2022-08-17 12:34:02 +05:30
Mohammad Shoaib Khan
d309844b28 Removed unnecessary code 2022-08-14 20:19:09 +05:30
Mohammad Shoaib Khan
b4625ddfc8 Issue#29 Fixed crab pot not getting detected as a machine 2022-08-14 20:09:09 +05:30
Mohammad Shoaib Khan
f2d607484e Issue#27 Added mill input and output tiles detection 2022-08-14 19:50:53 +05:30
Mohammad Shoaib Khan
18a7dc5d23 Issue#33 Fixed rings bug in crafting page 2022-08-13 14:21:22 +05:30
Mohammad Shoaib Khan
408a40121d Issue#25 Added command to toggle speaking in percentage for health n stamina 2022-08-13 13:06:24 +05:30
Mohammad Shoaib Khan
949e5c5bfc Issue#23 Added Signs to read tile 2022-08-13 12:15:38 +05:30
Mohammad Shoaib Khan
9199d764ba Issue#23 Added indoor pots i.e. garden pot to read tile 2022-08-13 12:00:45 +05:30
Mohammad Shoaib Khan
df192ee90c Added docs and replaced escape key with enter 2022-08-12 16:37:39 +05:30
Mohammad Shoaib
4a1e4f9f4a Bug fix 2022-06-10 22:50:06 +05:30
Mohammad Shoaib
5b37859216 Fixed advanced game options menu 2022-06-06 22:43:25 +05:30
Mohammad Shoaib
1e096cf521 Release 1.3.0 2022-06-06 22:40:37 +05:30
Mohammad Shoaib
617251c3de Patched advanced options menu 2022-06-06 22:38:51 +05:30
Mohammad Shoaib
157bdd0150 Fixed moving animal 2022-06-06 22:27:04 +05:30
Mohammad Shoaib
e534a6aed9 Fixed move animal to different building 2022-05-30 23:38:09 +05:30
Mohammad Shoaib
9c1a185047 Revamped character creation menu 2022-05-30 22:34:58 +05:30
Mohammad Shoaib
d48e4cc1b6 Merge pull request #21 from bradjrenshaw/GingerIslandAdditions
Ginger island additions
2022-05-27 18:11:12 +05:30
bradjrenshaw
9f8d4273bc Fixed static tiles json formatting; fixed a few entrances with missing coordinates. 2022-05-18 19:16:44 -04:00
Mohammad Shoaib
93c75fca65 Merge pull request #20 from xyzz2001/patch-3
Update zh.json
2022-05-16 22:31:49 +05:30
Mohammad Shoaib
9c2931ec2f Added all keybinds to mod config 2022-05-16 22:01:02 +05:30
Mohammad Shoaib
923ad61580 Changed b key to c in buildingNAnimal menu 2022-05-16 21:35:01 +05:30
Mohammad Shoaib
b1056a6cd1 Added chat menu keys to mod config 2022-05-16 21:34:19 +05:30
Mohammad Shoaib
f85192178f Read tile pauses while auto walking 2022-05-16 21:07:08 +05:30
bradjrenshaw
661d6bb31f Add secondary entrances connecting various locations (Island North to south and Island North to Volcano Dungeon) 2022-05-15 18:58:23 -04:00
bradjrenshaw
7b81660dd2 Add shipwreck entrance and exit to static tiles. 2022-05-15 18:33:24 -04:00
xywlkj2001_星云
6a49fccb9c Update zh.json 2022-05-16 04:22:07 +08:00
Mohammad Shoaib
fcd6c8ecb3 Moved sounds to assets folder 2022-05-15 15:30:31 +05:30
Mohammad Shoaib
0942eec32e beta 1.2.3 2022-05-15 15:26:34 +05:30
Mohammad Shoaib
80aa0bf106 Added translations to other languages 2022-05-15 15:22:50 +05:30
Mohammad Shoaib
af50fd114c Cleaned the code a bit 2022-05-15 15:03:04 +05:30
Mohammad Shoaib
7f81d72771 added more scenes to grandpa story 2022-05-15 15:00:08 +05:30
Mohammad Shoaib
f9dd84efc8 Patched grandpa story and intro mini game 2022-05-15 13:28:20 +05:30
Mohammad Shoaib
4dcef714fa Fixed warning 2022-05-15 12:30:52 +05:30
Mohammad Shoaib
373a39fa33 Added pet's water bowl 2022-05-15 12:29:40 +05:30
Mohammad Shoaib
715601f694 Added footstep sounds to auto walk 2022-05-15 12:29:40 +05:30
Mohammad Shoaib
cb49832b5a Player stops auto walking if WASD is pressed 2022-05-15 12:29:40 +05:30
Mohammad Shoaib
c8b274347e Added auto walk feature to tile viewer 2022-05-15 12:29:40 +05:30
Mohammad Shoaib
3c76c424e8 Merge pull request #18 from bradjrenshaw/GemBirds
Ginger Island gem birds added to TileInfo npcs category.
2022-05-15 12:29:13 +05:30
bradjrenshaw
9e0c2dfab7 Fix incorrectly named Topaz Gem Bird. 2022-05-15 01:43:39 -04:00
bradjrenshaw
e4187a636a Ginger Island gem birds added to TileInfo npcs category. 2022-05-15 00:22:36 -04:00
Mohammad Shoaib
bd806c077a Merge pull request #17 from bradjrenshaw/MiscFixes
Misc fixes
2022-05-14 08:15:53 +05:30
bradjrenshaw
f4a47bf64a Added ore panning spot to interactables category. 2022-05-12 20:33:50 -04:00
bradjrenshaw
153b120730 Added old mariner to npcs category when present. 2022-05-12 20:05:01 -04:00
bradjrenshaw
569311c61e Fix incorrect AM and PM time announcement and :00 formatting. 2022-05-12 20:04:33 -04:00
Mohammad Shoaib
d74ca682eb Added time warning 2022-05-12 23:22:59 +05:30
Mohammad Shoaib
6c04ba7bf4 Removed pause from hud message as it was pausing too much 2022-05-12 22:59:40 +05:30
Mohammad Shoaib
92b7230a4d Fix for stone types 2022-05-12 22:54:33 +05:30
Mohammad Shoaib
f8a6cab24e Added missing entrances 2022-05-12 21:47:01 +05:30
Mohammad Shoaib
ebc68f0ffa Added pause option to config
It now pauses in hud messages
2022-05-12 16:00:20 +05:30
Mohammad Shoaib
92d5ae22d9 Added other languages to warning message 2022-05-12 15:56:05 +05:30
Mohammad Shoaib
2d74653217 Read tile pauses when narrating a warning message 2022-05-12 15:28:09 +05:30
Mohammad Shoaib
d8dbfe78d4 Added i18n folder for other language support
and logic for narrating warnings
2022-05-11 13:47:19 +05:30
Mohammad Shoaib
04ba44d045 Added warnings.cs 2022-05-11 13:29:12 +05:30
Mohammad Shoaib
eb25243377 Code documentation 2022-05-11 13:14:46 +05:30
Mohammad Shoaib
d27c7c749c Merge pull request #16 from bradjrenshaw/TileViewer
Tile viewer
2022-05-11 13:00:04 +05:30
bradjrenshaw
55b9255dc8 Formatting and code cleanup; added documentation and some clarifying comments. 2022-05-10 17:30:40 -04:00
bradjrenshaw
8446049c55 Rename MouseHandler to TileViewer for accuracy 2022-05-10 16:36:23 -04:00
bradjrenshaw
7a1b768bdb Added option to allow tile cursor to view the entire map regardless if it is visible or not. 2022-05-10 16:31:54 -04:00
bradjrenshaw
955ffac65b Implementation of relative offset lock (keep mouse in position relative to you as yo umove). 2022-05-10 15:22:32 -04:00
bradjrenshaw
c8bf29b6d0 Added precise tile cursor movement (defaults to 8 pixels per key press). 2022-05-10 15:09:54 -04:00
bradjrenshaw
a8bcaf6cef Refactored keyboard tile cursor input to be triggered at the same time as the rest of keyboard input; fix bug in reading UI components with tile cursor. 2022-05-10 14:37:43 -04:00
bradjrenshaw
332dd858a8 Fixed bug with tile cursor not passing exact integer tile values causing terrain features like crops to not be read out. 2022-05-10 14:21:17 -04:00
Mohammad Shoaib
c49dec4f29 Beta 1.2.2 2022-05-10 20:55:51 +05:30
Mohammad Shoaib
78a466a010 Added category to manual read tile key 2022-05-10 20:47:40 +05:30
Mohammad Shoaib
7688bc0dd5 Fixed giant crop narration 2022-05-10 20:47:21 +05:30
Mohammad Shoaib
6acfd313e8 Added quartz, etc. to mine item category 2022-05-10 20:05:02 +05:30
Mohammad Shoaib
cedf2dcc03 Added area narration when switching to other area 2022-05-10 19:48:04 +05:30
Mohammad Shoaib
672f9a1f00 Added twig to debris category 2022-05-10 19:39:22 +05:30
bradjrenshaw
838273b3ce Read blocked or empty tiles if no object is on a tile. 2022-05-10 00:05:35 -04:00
bradjrenshaw
bc6ec49619 Initial implementation of cursor movement and mouse snapping to cursor location. 2022-05-09 23:53:29 -04:00
bradjrenshaw
2717efa42e Merge branch 'master' into TileViewer 2022-05-09 20:05:12 -04:00
Mohammad Shoaib
730beeffaa removed birdie from static tiles
(as she was already detectable)
2022-05-10 00:21:37 +05:30
Mohammad Shoaib
eebabe8fe6 Added statue of perfection & endless fortune 2022-05-10 00:02:15 +05:30
Mohammad Shoaib
415231c65d Added farm and farm cave tiles 2022-05-09 23:29:21 +05:30
Mohammad Shoaib
73fb957a46 Merge pull request #15 from bradjrenshaw/WalnutLocationFix
Walnut location fix
2022-05-09 20:59:54 +05:30
bradjrenshaw
72f1835929 Brief refactor for readability. 2022-05-08 19:31:04 -04:00
bradjrenshaw
43c3dbb0d8 Added diggable golden walnut locations to ginger Island tileInfo. 2022-05-08 19:22:28 -04:00
bradjrenshaw
0b318b29ad MouseHandler refactoring. 2022-05-08 18:46:37 -04:00
bradjrenshaw
797e0ab136 Add cursor keys to mod config. 2022-05-08 01:55:38 -04:00
bradjrenshaw
8dd96d84b3 Refactoring mouse handling into a separate class. 2022-05-08 01:47:32 -04:00
bradjrenshaw
bd29fae9ca Initial refactor of mouse handling. 2022-05-06 19:47:11 -04:00
Mohammad Shoaib
f9fbbc4181 Moved static-tiles.json to assets folder 2022-05-05 00:08:17 +05:30
Mohammad Shoaib
8355f05684 Added event tiles 2022-05-05 00:01:56 +05:30
Mohammad Shoaib
4cc4bba62e Added trailer tiles 2022-05-04 23:33:05 +05:30
Mohammad Shoaib
14506b1e11 Added Josh House tiles 2022-05-04 23:29:15 +05:30
Mohammad Shoaib
297eca3adc Added haley house tiles 2022-05-02 23:43:43 +05:30
Mohammad Shoaib
1aa569ab84 Added sam house tiles 2022-05-02 23:31:17 +05:30
Mohammad Shoaib
b287021db7 Added science house tiles 2022-05-02 23:26:41 +05:30
Mohammad Shoaib
110989ebcc Added saloon tiles 2022-05-02 23:10:50 +05:30
Mohammad Shoaib
56c17bf96d Added animal shop tiles 2022-05-02 23:02:22 +05:30
Mohammad Shoaib
03c3ba4981 Added tiles for hospital & blacksmith 2022-05-02 22:46:08 +05:30
Mohammad Shoaib
4735bde498 Added secret woods tiles 2022-05-02 22:23:06 +05:30
Mohammad Shoaib
a6f8e5fe35 Added wizard house tiles 2022-05-02 14:36:17 +05:30
Mohammad Shoaib
c00ae77d03 Added sewer tiles 2022-05-02 14:28:13 +05:30
Mohammad Shoaib
03e8eead73 Added seed shop tiles 2022-05-02 14:21:25 +05:30
Mohammad Shoaib
83a26bb04a Added movie theater tiles 2022-05-02 14:12:17 +05:30
Mohammad Shoaib
c2630ffb70 Added mountain, backwoods, railroad tiles 2022-05-02 13:41:56 +05:30
Mohammad Shoaib
825c8b6a56 Added mine tiles 2022-05-02 13:06:49 +05:30
Mohammad Shoaib
5cdf1da37f Added manor house tiles 2022-05-02 13:02:02 +05:30
Mohammad Shoaib
130204e39e Added mail box to read tile 2022-05-02 12:54:04 +05:30
Mohammad Shoaib
ce69a5ad98 Added SearchLocation() to API 2022-05-01 14:26:08 +05:30
Mohammad Shoaib
6acfb4ff4e Implemented BFS to SearchLocation() 2022-05-01 14:17:31 +05:30
Mohammad Shoaib
c3c0169ac6 Added method to scan entire location for tiles 2022-05-01 13:20:18 +05:30
Mohammad Shoaib
666dfd745b Added library museum tiles 2022-04-30 23:45:58 +05:30
Mohammad Shoaib
33b42b0901 Added joja mart tiles 2022-04-30 23:43:00 +05:30
Mohammad Shoaib
d3055f7cbe Added ginger island tiles and stuff 2022-04-30 23:37:41 +05:30
Mohammad Shoaib
b16fa50cd2 Added community center exit 2022-04-30 22:37:35 +05:30
Mohammad Shoaib
45ad9ec42f Added cellar exit 2022-04-30 22:34:31 +05:30
Mohammad Shoaib
ababbcbe07 Added Beach night market static tiles 2022-04-30 22:29:20 +05:30
Mohammad Shoaib
d933152982 Added forest static & dynamic tiles 2022-04-30 22:09:53 +05:30
Mohammad Shoaib
cb450b95fa Added exits for farm houses 2022-04-30 20:23:17 +05:30
Mohammad Shoaib
97fabf7638 Added building human and animal doors to read tile 2022-04-30 20:16:51 +05:30
Mohammad Shoaib
e1e75ab449 Added exit points to existing maps 2022-04-30 20:04:54 +05:30
Mohammad Shoaib
08af64d560 Added fish shop and boat tunnel static tiles 2022-04-29 20:22:56 +05:30
Mohammad Shoaib
9e28399a0e Added desert static tiles 2022-04-29 20:00:26 +05:30
Mohammad Shoaib
c4290173fd Added missed rewards bundle to read tile 2022-04-29 19:46:26 +05:30
Mohammad Shoaib
2dbc52a994 Added club/casino static tiles 2022-04-29 19:41:30 +05:30
Mohammad Shoaib
d4e31f8a1a Added caldera static tiles 2022-04-29 00:12:13 +05:30
Mohammad Shoaib
4dff690bbe Added beach bridge 2022-04-28 23:50:26 +05:30
Mohammad Shoaib
fb7791e7ba Added adventure guild static tiles 2022-04-28 23:12:53 +05:30
Mohammad Shoaib
4db3c305f3 Added slime hutch static tiles 2022-04-28 23:05:52 +05:30
Mohammad Shoaib
c0ad4b83ab Code organization 2022-04-28 22:54:19 +05:30
Mohammad Shoaib
253d01ac6f Added barn to static tiles 2022-04-28 20:12:07 +05:30
Mohammad Shoaib
5355a2015e Added more dynamic tiles 2022-04-28 20:00:27 +05:30
Mohammad Shoaib
d9f99ac922 Added static tiles in coops 2022-04-24 13:14:52 +05:30
Mohammad Shoaib
bafc966072 Added command to refresh static tiles json 2022-04-24 13:14:34 +05:30
Mohammad Shoaib
2a03fa6724 Added interactables to town location 2022-04-24 12:26:53 +05:30
Mohammad Shoaib
21c5080f73 Added more to the default exclusion list 2022-04-24 12:14:18 +05:30
Mohammad Shoaib
864ad56cfc Added bridge in CATEGORY 2022-04-24 12:10:13 +05:30
Mohammad Shoaib
4082cc033b multiple values for same tile 2022-04-24 11:57:14 +05:30
Mohammad Shoaib
98b4247cbb Bug fix 2022-04-23 18:29:35 +05:30
Mohammad Shoaib
ca8ecd845b Added category to static tiles 2022-04-23 18:21:50 +05:30
Mohammad Shoaib
5148139825 Added static tiles to tile info 2022-04-23 17:54:19 +05:30
Mohammad Shoaib
4590f64736 Added quest progress for special order quests 2022-04-23 16:56:22 +05:30
Mohammad Shoaib
bfdb7c9f5b Bug fix in quest log patch 2022-04-23 16:34:53 +05:30
Mohammad Shoaib
49ec3fc85b Fixed stuttering in animal menu 2022-04-23 15:35:36 +05:30
Mohammad Shoaib
d051e4359c Fixed stuttering in pond query menu 2022-04-23 15:30:47 +05:30
Mohammad Shoaib
d9bb4aac35 Bug fix in read tile 2022-04-15 16:02:24 +05:30
Mohammad Shoaib
c3882e7ea9 Added lava and cooled lava to read tile 2022-04-15 15:28:19 +05:30
Mohammad Shoaib
a0b6159df8 Fixed bug 2022-04-15 14:49:40 +05:30
Mohammad Shoaib
4351182984 Organized code and added parrot perch to read tile 2022-04-15 14:27:52 +05:30
Mohammad Shoaib
f0a85e0be9 Patched field office menu 2022-04-14 12:59:23 +05:30
Mohammad Shoaib
559922c0a9 Fixed item list menu 2022-04-14 12:34:05 +05:30
Mohammad Shoaib
ffe4fff5d2 Patched item list menu 2022-04-14 12:08:00 +05:30
Mohammad Shoaib
bf9ca47dc4 Patched forge menu 2022-04-12 23:54:46 +05:30
Mohammad Shoaib
3b55110e11 Patched pond query menu 2022-04-12 23:37:34 +05:30
Mohammad Shoaib
38f32c2b48 Patched tailoring menu 2022-04-12 22:50:36 +05:30
Mohammad Shoaib
a19186d30d Patching tailoring menu 2022-04-12 00:00:00 +05:30
Mohammad Shoaib
a1a7ed9281 Bug fixes 2022-04-11 23:48:20 +05:30
Mohammad Shoaib
0e1ab968e7 Patched joja community development menu 2022-04-11 22:48:56 +05:30
Mohammad Shoaib
4b6879b1f3 Patched choose from list(jukebox) menu 2022-04-09 17:11:14 +05:30
Mohammad Shoaib
9e1c263cf8 Fixed warnings 2022-04-09 15:48:13 +05:30
Mohammad Shoaib
416b573f43 Fixed naming menu 2022-04-09 15:15:50 +05:30
Mohammad Shoaib
6c7b845f15 Added keybind to donate item to museum 2022-04-08 17:07:32 +05:30
Mohammad Shoaib
8bd44fe360 Fixed bug in crafting/cooking menu 2022-04-08 15:50:57 +05:30
Mohammad Shoaib
025956f66c Added recipe skipping when pressing c if it is not unlocked 2022-04-05 17:37:09 +05:30
Mohammad Shoaib
afc54a0eef Patched Animal Query Menu 2022-04-05 17:21:35 +05:30
Mohammad Shoaib
041afa68a0 Added check for unknown recipe in crafting/cooking menu 2022-04-05 13:13:11 +05:30
Mohammad Shoaib
53f7e3ceb0 Patched collections page's letter viewer menu 2022-04-05 12:44:32 +05:30
Mohammad Shoaib
9fe91faeee Organized code 2022-04-05 12:17:28 +05:30
Mohammad Shoaib
7d18bbce44 Fixed back button not working in character customization 2022-04-02 16:50:40 +05:30
Mohammad Shoaib
dc853a1d7c Added co-op related options to character customization menu 2022-04-02 16:33:53 +05:30
Mohammad Shoaib
7ecf7fb30c Added pet to character customization menu 2022-04-02 15:49:18 +05:30
Mohammad Shoaib
bfad0676bd Code Cleanup 2022-03-31 14:31:09 +05:30
Mohammad Shoaib
7063305510 Fixed fullscreen indication 2022-03-29 19:59:47 +05:30
Mohammad Shoaib
d24c224d1d Code cleanup 2022-03-29 19:57:38 +05:30
Mohammad Shoaib
5d8912ae7c Added fullscreen indication 2022-03-29 19:25:53 +05:30
Mohammad Shoaib
101d0f496c Removed duplicate code 2022-03-21 12:33:44 +05:30
Mohammad Shoaib
fb087476d2 Added mod config with keybindings 2022-03-21 12:12:50 +05:30
Mohammad Shoaib
9607a008e0 Added mod config 2022-03-21 11:13:55 +05:30
Mohammad Shoaib
3b4bea69f2 Merge pull request #12 from bradjrenshaw/master
added config and option to toggle coordinate verbosity
2022-03-21 10:58:05 +05:30
bradjrenshaw
f9b4bc88db added config and option to toggle coordinate verbosity; fixed coordinate formatting of comma. 2022-03-20 16:26:30 -04:00
Mohammad Shoaib
b3a9e97016 more readability 2022-03-20 13:04:24 +05:30
Mohammad Shoaib
b47e1b5cd6 Beta 1.1.2 2022-03-20 12:35:50 +05:30
Mohammad Shoaib
2a003fc6ba Fixed busy stone 2022-03-20 12:26:01 +05:30
Mohammad Shoaib
b3bad018dc Merge pull request #11 from TrueBlindGaming/master
fix for busy stones..mostly
2022-03-20 12:10:12 +05:30
TrueBlindGaming
b7e01c1dc1 fix for missing bundle 2022-03-19 16:59:42 -06:00
TrueBlindGaming
eb68e61bf7 fix for busy stones..mostly 2022-03-19 12:25:07 -06:00
Mohammad Shoaib
092960f571 Fixed museum menu 2022-03-19 15:44:57 +05:30
Mohammad Shoaib
32d354f23c Fixing museum menu 2022-03-19 15:23:18 +05:30
Mohammad Shoaib
5b0c6fe154 Fixed letter viewer menu patch 2022-03-19 14:30:37 +05:30
Mohammad Shoaib
49be1d3e6b Bug fix in bundle menu 2022-03-19 14:12:47 +05:30
Mohammad Shoaib
99961d8e62 Added alt + k to narrate current location name 2022-03-19 14:03:21 +05:30
Mohammad Shoaib
18a671d97a Added the Missing Bundle 2022-03-19 13:57:55 +05:30
Mohammad Shoaib
5e65ca8dbd Added the Missing Bundle 2022-03-19 13:12:45 +05:30
Mohammad Shoaib
2d3d79be04 Added alt + j keybind 2022-03-19 12:58:24 +05:30
Mohammad Shoaib
674cd80398 Added machine status 2022-03-19 12:58:24 +05:30
Mohammad Shoaib
fcd2243cc5 Made reading floorings toggleable | Added names to tuples making it more readable 2022-03-19 12:58:24 +05:30
Mohammad Shoaib
0de1cd419b Removed [ ] from charachter customization menu 2022-03-19 12:58:24 +05:30
121 changed files with 13853 additions and 5440 deletions

6
.gitattributes vendored
View File

@@ -61,3 +61,9 @@
#*.PDF diff=astextplain #*.PDF diff=astextplain
#*.rtf diff=astextplain #*.rtf diff=astextplain
#*.RTF diff=astextplain #*.RTF diff=astextplain
# Keep line endings consistent for files.
*.cs eol=crlf
*.json eol=crlf
LICENSE* eol=crlf

5
.gitignore vendored
View File

@@ -3,7 +3,12 @@
## ##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
*.dll
stardew-access/assets/custom-tiles.json
.vscode/* .vscode/*
.git-old/
bin/
# User-specific files # User-specific files
*.rsuser *.rsuser

504
LICENSE-NVDA Normal file
View File

@@ -0,0 +1,504 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

165
LICENSE-TOLK Normal file
View File

@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. [http://fsf.org/]
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@@ -0,0 +1,32 @@
PIXABAY LICENSE CERTIFICATE
==============================================
This document confirms the download of an audio file pursuant to the Pixabay License as defined in the Pixabay Terms of Service available at https://https://pixabay.com/service/terms/
Licensor's Username:
https://pixabay.com/users/pixabay-1/
Licensee:
TheOneWhoKnocks
Audio File Title:
Invalid Selection
Audio File URL:
https://pixabay.com/sound-effects/invalid-selection-39351/
Audio File ID:
39351
Date of download:
2023-02-20 05:52:26 UTC
Pixabay GmbH c/o Lacore Rechtsanwälte LLP
Berliner Freiheit 2, 10785 Berlin, Germany
Pixabay is a user-contributed stock content website. The above-named Licensor is responsible for this audio file. Pixabay monitors uploaded audio files only to a reasonable extent. Pixabay cannot be held responsible for the acts or omissions of its users and does not represent or warrant that any required third-party consents or licenses have been obtained.
For any queries related to this document please contact Pixabay via info@pixabay.com.
==== THIS IS NOT A TAX RECEIPT OR INVOICE ====

View File

@@ -16,13 +16,13 @@ namespace stardew_access.ScreenReader
/// <param name="center">The starting point.</param> /// <param name="center">The starting point.</param>
/// <param name="limit">The limiting factor or simply radius of the search area.</param> /// <param name="limit">The limiting factor or simply radius of the search area.</param>
/// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns> /// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns>
public Dictionary<Vector2, (string, string)> SearchNearbyTiles(Vector2 center, int limit) public Dictionary<Vector2, (string name, string category)> SearchNearbyTiles(Vector2 center, int limit)
{ {
/* /*
* How to use the Dictionary to get the name and category of a tile:- * How to use the Dictionary to get the name and category of a tile:-
* *
* string? objectName = detectedTiles.GetValueOrDefault(center).Item1; * string tileName = detectedTiles.GetValueOrDefault(tilePosition).name;
* string? objectCategory = detectedTiles.GetValueOrDefault(center).Item2; * string tileCategory = detectedTiles.GetValueOrDefault(tilePosition).category;
* *
* Here detectedTiles is the Dictionary returned by this method * Here detectedTiles is the Dictionary returned by this method
*/ */
@@ -30,14 +30,32 @@ namespace stardew_access.ScreenReader
return new Radar().SearchNearbyTiles(center, limit, false); return new Radar().SearchNearbyTiles(center, limit, false);
} }
/// <summary>
/// Search the entire location using Breadth First Search algorithm(BFS).
/// </summary>
/// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns>
public Dictionary<Vector2, (string name, string category)> SearchLocation()
{
/*
* How to use the Dictionary to get the name and category of a tile:-
*
* string tileName = detectedTiles.GetValueOrDefault(tilePosition).name;
* string tileCategory = detectedTiles.GetValueOrDefault(tilePosition).category;
*
* Here detectedTiles is the Dictionary returned by this method
*/
return new Radar().SearchLocation();
}
/// <summary> /// <summary>
/// Check the tile for any object /// Check the tile for any object
/// </summary> /// </summary>
/// <param name="tile">The tile where we want to check the name and category of object if any</param> /// <param name="tile">The tile where we want to check the name and category of object if any</param>
/// <returns>Name of the object as the first item (Item1) and category as the second item (Item2). Returns null if no object found.</returns> /// <returns>Name of the object as the first item (name) and category as the second item (category). Returns null if no object found.</returns>
public (string?, string?) GetNameWithCategoryNameAtTile(Vector2 tile) public (string? name, string? category) GetNameWithCategoryNameAtTile(Vector2 tile)
{ {
return ReadTile.getNameWithCategoryNameAtTile(tile); return TileInfo.getNameWithCategoryNameAtTile(tile, null);
} }
/// <summary> /// <summary>
@@ -47,7 +65,7 @@ namespace stardew_access.ScreenReader
/// <returns>Name of the object. Returns null if no object found.</returns> /// <returns>Name of the object. Returns null if no object found.</returns>
public string? GetNameAtTile(Vector2 tile) public string? GetNameAtTile(Vector2 tile)
{ {
return ReadTile.getNameAtTile(tile); return TileInfo.GetNameAtTile(tile, null);
} }
/// <summary>Speaks the text via the loaded screen reader (if any).</summary> /// <summary>Speaks the text via the loaded screen reader (if any).</summary>
@@ -55,10 +73,10 @@ namespace stardew_access.ScreenReader
/// <param name="interrupt">Whether to skip the currently speaking text or not.</param> /// <param name="interrupt">Whether to skip the currently speaking text or not.</param>
public void Say(String text, Boolean interrupt) public void Say(String text, Boolean interrupt)
{ {
if (MainClass.GetScreenReader() == null) if (MainClass.ScreenReader == null)
return; return;
MainClass.GetScreenReader().Say(text, interrupt); MainClass.ScreenReader.Say(text, interrupt);
} }
/// <summary>Speaks the text via the loaded screen reader (if any). /// <summary>Speaks the text via the loaded screen reader (if any).
@@ -67,10 +85,10 @@ namespace stardew_access.ScreenReader
/// <param name="interrupt">Whether to skip the currently speaking text or not.</param> /// <param name="interrupt">Whether to skip the currently speaking text or not.</param>
public void SayWithChecker(String text, Boolean interrupt) public void SayWithChecker(String text, Boolean interrupt)
{ {
if (MainClass.GetScreenReader() == null) if (MainClass.ScreenReader == null)
return; return;
MainClass.GetScreenReader().SayWithChecker(text, interrupt); MainClass.ScreenReader.SayWithChecker(text, interrupt);
} }
/// <summary>Speaks the text via the loaded screen reader (if any). /// <summary>Speaks the text via the loaded screen reader (if any).
@@ -80,10 +98,10 @@ namespace stardew_access.ScreenReader
/// <param name="interrupt">Whether to skip the currently speaking text or not.</param> /// <param name="interrupt">Whether to skip the currently speaking text or not.</param>
public void SayWithMenuChecker(String text, Boolean interrupt) public void SayWithMenuChecker(String text, Boolean interrupt)
{ {
if (MainClass.GetScreenReader() == null) if (MainClass.ScreenReader == null)
return; return;
MainClass.GetScreenReader().SayWithMenuChecker(text, interrupt); MainClass.ScreenReader.SayWithMenuChecker(text, interrupt);
} }
/// <summary>Speaks the text via the loaded screen reader (if any). /// <summary>Speaks the text via the loaded screen reader (if any).
@@ -93,10 +111,10 @@ namespace stardew_access.ScreenReader
/// <param name="interrupt">Whether to skip the currently speaking text or not.</param> /// <param name="interrupt">Whether to skip the currently speaking text or not.</param>
public void SayWithChatChecker(String text, Boolean interrupt) public void SayWithChatChecker(String text, Boolean interrupt)
{ {
if (MainClass.GetScreenReader() == null) if (MainClass.ScreenReader == null)
return; return;
MainClass.GetScreenReader().SayWithChatChecker(text, interrupt); MainClass.ScreenReader.SayWithChatChecker(text, interrupt);
} }
/// <summary>Speaks the text via the loaded screen reader (if any). /// <summary>Speaks the text via the loaded screen reader (if any).
@@ -108,10 +126,10 @@ namespace stardew_access.ScreenReader
/// <param name="interrupt">Whether to skip the currently speaking text or not.</param> /// <param name="interrupt">Whether to skip the currently speaking text or not.</param>
public void SayWithTileQuery(String text, int x, int y, Boolean interrupt) public void SayWithTileQuery(String text, int x, int y, Boolean interrupt)
{ {
if (MainClass.GetScreenReader() == null) if (MainClass.ScreenReader == null)
return; return;
MainClass.GetScreenReader().SayWithTileQuery(text, x, y, interrupt); MainClass.ScreenReader.SayWithTileQuery(text, x, y, interrupt);
} }
} }

View File

@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using stardew_access.Features;
using stardew_access.Patches; using stardew_access.Patches;
using StardewModdingAPI; using StardewModdingAPI;
using StardewValley; using StardewValley;
@@ -11,52 +12,69 @@ namespace stardew_access
{ {
internal static void Initialize() internal static void Initialize()
{ {
IModHelper helper = MainClass.ModHelper; //TODO organise this, create separate method for all commands
IModHelper? helper = MainClass.ModHelper;
if (helper == null)
return;
helper.ConsoleCommands.Add("readtile", "Toggle read tile feature.", (string commmand, string[] args) => #region Read Tile
helper.ConsoleCommands.Add("readtile", "Toggle read tile feature.", (string command, string[] args) =>
{
MainClass.Config.ReadTile = !MainClass.Config.ReadTile;
helper.WriteConfig(MainClass.Config);
MainClass.InfoLog("Read Tile is " + (MainClass.Config.ReadTile ? "on" : "off"));
});
helper.ConsoleCommands.Add("flooring", "Toggle flooring in read tile.", (string command, string[] args) =>
{ {
MainClass.readTile = !MainClass.readTile; MainClass.Config.ReadFlooring = !MainClass.Config.ReadFlooring;
helper.WriteConfig(MainClass.Config);
MainClass.GetMonitor().Log("Read Tile is " + (MainClass.readTile ? "on" : "off"), LogLevel.Info); MainClass.InfoLog("Flooring is " + (MainClass.Config.ReadFlooring ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("snapmouse", "Toggle snap mouse feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("watered", "Toggle speaking watered or unwatered for crops.", (string command, string[] args) =>
{ {
MainClass.snapMouse = !MainClass.snapMouse; MainClass.Config.WateredToggle = !MainClass.Config.WateredToggle;
helper.WriteConfig(MainClass.Config);
MainClass.GetMonitor().Log("Snap Mouse is " + (MainClass.snapMouse ? "on" : "off"), LogLevel.Info); MainClass.InfoLog("Watered toggle is " + (MainClass.Config.WateredToggle ? "on" : "off"));
});
helper.ConsoleCommands.Add("radar", "Toggle radar feature.", (string commmand, string[] args) =>
{
MainClass.radar = !MainClass.radar;
MainClass.GetMonitor().Log("Radar " + (MainClass.radar ? "on" : "off"), LogLevel.Info);
}); });
#endregion
#region Radar Feature #region Radar Feature
helper.ConsoleCommands.Add("rdebug", "Toggle debugging in radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("radar", "Toggle radar feature.", (string command, string[] args) =>
{
MainClass.Config.Radar = !MainClass.Config.Radar;
helper.WriteConfig(MainClass.Config);
MainClass.InfoLog("Radar " + (MainClass.Config.Radar ? "on" : "off"));
});
helper.ConsoleCommands.Add("rdebug", "Toggle debugging in radar feature.", (string command, string[] args) =>
{ {
MainClass.radarDebug = !MainClass.radarDebug; MainClass.radarDebug = !MainClass.radarDebug;
MainClass.GetMonitor().Log("Radar debugging " + (MainClass.radarDebug ? "on" : "off"), LogLevel.Info); MainClass.InfoLog("Radar debugging " + (MainClass.radarDebug ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rstereo", "Toggle stereo sound in radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rstereo", "Toggle stereo sound in radar feature.", (string command, string[] args) =>
{ {
MainClass.radarStereoSound = !MainClass.radarStereoSound; MainClass.Config.RadarStereoSound = !MainClass.Config.RadarStereoSound;
helper.WriteConfig(MainClass.Config);
MainClass.GetMonitor().Log("Stereo sound is " + (MainClass.radarStereoSound ? "on" : "off"), LogLevel.Info); MainClass.InfoLog("Stereo sound is " + (MainClass.Config.RadarStereoSound ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rfocus", "Toggle focus mode in radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfocus", "Toggle focus mode in radar feature.", (string command, string[] args) =>
{ {
bool focus = MainClass.RadarFeature.ToggleFocus(); bool focus = MainClass.RadarFeature.ToggleFocus();
MainClass.GetMonitor().Log("Focus mode is " + (focus ? "on" : "off"), LogLevel.Info); MainClass.InfoLog("Focus mode is " + (focus ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rdelay", "Set the delay of radar feature in milliseconds.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rdelay", "Set the delay of radar feature in milliseconds.", (string command, string[] args) =>
{ {
string? delayInString = null; string? delayInString = null;
@@ -72,24 +90,24 @@ namespace stardew_access
{ {
MainClass.RadarFeature.delay = delay; MainClass.RadarFeature.delay = delay;
if (delay >= 1000) if (delay >= 1000)
MainClass.GetMonitor().Log($"Delay set to {MainClass.RadarFeature.delay} milliseconds.", LogLevel.Info); MainClass.InfoLog($"Delay set to {MainClass.RadarFeature.delay} milliseconds.");
else else
MainClass.GetMonitor().Log($"Delay should be atleast 1 second or 1000 millisecond long.", LogLevel.Info); MainClass.InfoLog($"Delay should be atleast 1 second or 1000 millisecond long.");
} }
else else
{ {
MainClass.GetMonitor().Log("Invalid delay amount, it can only be in numeric form.", LogLevel.Info); MainClass.InfoLog("Invalid delay amount, it can only be in numeric form.");
} }
} }
else else
{ {
MainClass.GetMonitor().Log("Enter the delay amount (in milliseconds)!", LogLevel.Info); MainClass.InfoLog("Enter the delay amount (in milliseconds)!");
} }
}); });
helper.ConsoleCommands.Add("rrange", "Set the range of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rrange", "Set the range of radar feature.", (string command, string[] args) =>
{ {
string? rangeInString = null; string? rangeInString = null;
@@ -105,26 +123,26 @@ namespace stardew_access
{ {
MainClass.RadarFeature.range = range; MainClass.RadarFeature.range = range;
if (range >= 2 && range <= 10) if (range >= 2 && range <= 10)
MainClass.GetMonitor().Log($"Range set to {MainClass.RadarFeature.range}.", LogLevel.Info); MainClass.InfoLog($"Range set to {MainClass.RadarFeature.range}.");
else else
MainClass.GetMonitor().Log($"Range should be atleast 2 and maximum 10.", LogLevel.Info); MainClass.InfoLog($"Range should be atleast 2 and maximum 10.");
} }
else else
{ {
MainClass.GetMonitor().Log("Invalid range amount, it can only be in numeric form.", LogLevel.Info); MainClass.InfoLog("Invalid range amount, it can only be in numeric form.");
} }
} }
else else
{ {
MainClass.GetMonitor().Log("Enter the range amount!", LogLevel.Info); MainClass.InfoLog("Enter the range amount!");
} }
}); });
#region Exclusions #region Exclusions
helper.ConsoleCommands.Add("readd", "Add an object key to the exclusions list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("readd", "Add an object key to the exclusions list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@@ -136,20 +154,20 @@ namespace stardew_access
if (!MainClass.RadarFeature.exclusions.Contains(keyToAdd)) if (!MainClass.RadarFeature.exclusions.Contains(keyToAdd))
{ {
MainClass.RadarFeature.exclusions.Add(keyToAdd); MainClass.RadarFeature.exclusions.Add(keyToAdd);
MainClass.GetMonitor().Log($"Added {keyToAdd} key to exclusions list.", LogLevel.Info); MainClass.InfoLog($"Added {keyToAdd} key to exclusions list.");
} }
else else
{ {
MainClass.GetMonitor().Log($"{keyToAdd} key already present in the list.", LogLevel.Info); MainClass.InfoLog($"{keyToAdd} key already present in the list.");
} }
} }
else else
{ {
MainClass.GetMonitor().Log("Unable to add the key to exclusions list.", LogLevel.Info); MainClass.InfoLog("Unable to add the key to exclusions list.");
} }
}); });
helper.ConsoleCommands.Add("reremove", "Remove an object key from the exclusions list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("reremove", "Remove an object key from the exclusions list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@@ -161,20 +179,20 @@ namespace stardew_access
if (MainClass.RadarFeature.exclusions.Contains(keyToAdd)) if (MainClass.RadarFeature.exclusions.Contains(keyToAdd))
{ {
MainClass.RadarFeature.exclusions.Remove(keyToAdd); MainClass.RadarFeature.exclusions.Remove(keyToAdd);
MainClass.GetMonitor().Log($"Removed {keyToAdd} key from exclusions list.", LogLevel.Info); MainClass.InfoLog($"Removed {keyToAdd} key from exclusions list.");
} }
else else
{ {
MainClass.GetMonitor().Log($"Cannot find {keyToAdd} key in exclusions list.", LogLevel.Info); MainClass.InfoLog($"Cannot find {keyToAdd} key in exclusions list.");
} }
} }
else else
{ {
MainClass.GetMonitor().Log("Unable to remove the key from exclusions list.", LogLevel.Info); MainClass.InfoLog("Unable to remove the key from exclusions list.");
} }
}); });
helper.ConsoleCommands.Add("relist", "List all the exclusions in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("relist", "List all the exclusions in the radar feature.", (string command, string[] args) =>
{ {
if (MainClass.RadarFeature.exclusions.Count > 0) if (MainClass.RadarFeature.exclusions.Count > 0)
{ {
@@ -183,28 +201,28 @@ namespace stardew_access
{ {
toPrint = $"{toPrint}\t{i + 1}: {MainClass.RadarFeature.exclusions[i]}"; toPrint = $"{toPrint}\t{i + 1}: {MainClass.RadarFeature.exclusions[i]}";
} }
MainClass.GetMonitor().Log(toPrint, LogLevel.Info); MainClass.InfoLog(toPrint);
} }
else else
{ {
MainClass.GetMonitor().Log("No exclusions found.", LogLevel.Info); MainClass.InfoLog("No exclusions found.");
} }
}); });
helper.ConsoleCommands.Add("reclear", "Clear the focus exclusions in the radar featrure.", (string commmand, string[] args) => helper.ConsoleCommands.Add("reclear", "Clear the focus exclusions in the radar featrure.", (string command, string[] args) =>
{ {
MainClass.RadarFeature.exclusions.Clear(); MainClass.RadarFeature.exclusions.Clear();
MainClass.GetMonitor().Log($"Cleared the focus list in the exclusions feature.", LogLevel.Info); MainClass.InfoLog($"Cleared the focus list in the exclusions feature.");
}); });
helper.ConsoleCommands.Add("recount", "Number of exclusions in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("recount", "Number of exclusions in the radar feature.", (string command, string[] args) =>
{ {
MainClass.GetMonitor().Log($"There are {MainClass.RadarFeature.exclusions.Count} exclusiond in the radar feature.", LogLevel.Info); MainClass.InfoLog($"There are {MainClass.RadarFeature.exclusions.Count} exclusiond in the radar feature.");
}); });
#endregion #endregion
#region Focus #region Focus
helper.ConsoleCommands.Add("rfadd", "Add an object key to the focus list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfadd", "Add an object key to the focus list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@@ -216,20 +234,20 @@ namespace stardew_access
if (!MainClass.RadarFeature.focus.Contains(keyToAdd)) if (!MainClass.RadarFeature.focus.Contains(keyToAdd))
{ {
MainClass.RadarFeature.focus.Add(keyToAdd); MainClass.RadarFeature.focus.Add(keyToAdd);
MainClass.GetMonitor().Log($"Added {keyToAdd} key to focus list.", LogLevel.Info); MainClass.InfoLog($"Added {keyToAdd} key to focus list.");
} }
else else
{ {
MainClass.GetMonitor().Log($"{keyToAdd} key already present in the list.", LogLevel.Info); MainClass.InfoLog($"{keyToAdd} key already present in the list.");
} }
} }
else else
{ {
MainClass.GetMonitor().Log("Unable to add the key to focus list.", LogLevel.Info); MainClass.InfoLog("Unable to add the key to focus list.");
} }
}); });
helper.ConsoleCommands.Add("rfremove", "Remove an object key from the focus list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfremove", "Remove an object key from the focus list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@@ -241,20 +259,20 @@ namespace stardew_access
if (MainClass.RadarFeature.focus.Contains(keyToAdd)) if (MainClass.RadarFeature.focus.Contains(keyToAdd))
{ {
MainClass.RadarFeature.focus.Remove(keyToAdd); MainClass.RadarFeature.focus.Remove(keyToAdd);
MainClass.GetMonitor().Log($"Removed {keyToAdd} key from focus list.", LogLevel.Info); MainClass.InfoLog($"Removed {keyToAdd} key from focus list.");
} }
else else
{ {
MainClass.GetMonitor().Log($"Cannot find {keyToAdd} key in focus list.", LogLevel.Info); MainClass.InfoLog($"Cannot find {keyToAdd} key in focus list.");
} }
} }
else else
{ {
MainClass.GetMonitor().Log("Unable to remove the key from focus list.", LogLevel.Info); MainClass.InfoLog("Unable to remove the key from focus list.");
} }
}); });
helper.ConsoleCommands.Add("rflist", "List all the exclusions in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rflist", "List all the exclusions in the radar feature.", (string command, string[] args) =>
{ {
if (MainClass.RadarFeature.focus.Count > 0) if (MainClass.RadarFeature.focus.Count > 0)
{ {
@@ -263,41 +281,41 @@ namespace stardew_access
{ {
toPrint = $"{toPrint}\t{i + 1}): {MainClass.RadarFeature.focus[i]}"; toPrint = $"{toPrint}\t{i + 1}): {MainClass.RadarFeature.focus[i]}";
} }
MainClass.GetMonitor().Log(toPrint, LogLevel.Info); MainClass.InfoLog(toPrint);
} }
else else
{ {
MainClass.GetMonitor().Log("No objects found in the focus list.", LogLevel.Info); MainClass.InfoLog("No objects found in the focus list.");
} }
}); });
helper.ConsoleCommands.Add("rfclear", "Clear the focus list in the radar featrure.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfclear", "Clear the focus list in the radar featrure.", (string command, string[] args) =>
{ {
MainClass.RadarFeature.focus.Clear(); MainClass.RadarFeature.focus.Clear();
MainClass.GetMonitor().Log($"Cleared the focus list in the radar feature.", LogLevel.Info); MainClass.InfoLog($"Cleared the focus list in the radar feature.");
}); });
helper.ConsoleCommands.Add("rfcount", "Number of list in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfcount", "Number of list in the radar feature.", (string command, string[] args) =>
{ {
MainClass.GetMonitor().Log($"There are {MainClass.RadarFeature.focus.Count} objects in the focus list in the radar feature.", LogLevel.Info); MainClass.InfoLog($"There are {MainClass.RadarFeature.focus.Count} objects in the focus list in the radar feature.");
}); });
#endregion #endregion
#endregion #endregion
#region Tile marking #region Tile marking
helper.ConsoleCommands.Add("mark", "Marks the player's position for use in building construction in Carpenter Menu.", (string commmand, string[] args) => helper.ConsoleCommands.Add("mark", "Marks the player's position for use in building construction in Carpenter Menu.", (string command, string[] args) =>
{ {
if (Game1.currentLocation is not Farm) if (Game1.currentLocation is not Farm)
{ {
MainClass.GetMonitor().Log("Can only use this command in the farm", LogLevel.Info); MainClass.InfoLog("Can only use this command in the farm");
return; return;
} }
string? indexInString = args.ElementAtOrDefault(0); string? indexInString = args.ElementAtOrDefault(0);
if (indexInString == null) if (indexInString == null)
{ {
MainClass.GetMonitor().Log("Enter the index too!", LogLevel.Info); MainClass.InfoLog("Enter the index too!");
return; return;
} }
@@ -306,76 +324,48 @@ namespace stardew_access
if (!isParsable || !(index >= 0 && index <= 9)) if (!isParsable || !(index >= 0 && index <= 9))
{ {
MainClass.GetMonitor().Log("Index can only be a number and from 0 to 9 only", LogLevel.Info); MainClass.InfoLog("Index can only be a number and from 0 to 9 only");
return; return;
} }
BuildingNAnimalMenuPatches.marked[index] = new Vector2((int)Game1.player.getTileX(), (int)Game1.player.getTileY()); BuildingOperations.marked[index] = new Vector2((int)Game1.player.getTileX(), (int)Game1.player.getTileY());
MainClass.GetMonitor().Log($"Location {(int)Game1.player.getTileX()}x {(int)Game1.player.getTileY()}y added at {index} index.", LogLevel.Info); MainClass.InfoLog($"Location {(int)Game1.player.getTileX()}x {(int)Game1.player.getTileY()}y added at {index} index.");
}); });
helper.ConsoleCommands.Add("marklist", "List all marked positions.", (string commmand, string[] args) => helper.ConsoleCommands.Add("marklist", "List all marked positions.", (string command, string[] args) =>
{ {
string toPrint = ""; string toPrint = "";
for (int i = 0; i < BuildingNAnimalMenuPatches.marked.Length; i++) for (int i = 0; i < BuildingOperations.marked.Length; i++)
{ {
if (BuildingNAnimalMenuPatches.marked[i] != Vector2.Zero) if (BuildingOperations.marked[i] != Vector2.Zero)
{ {
toPrint = $"{toPrint}\n Index {i}: {BuildingNAnimalMenuPatches.marked[i].X}x {BuildingNAnimalMenuPatches.marked[i].Y}y"; toPrint = $"{toPrint}\n Index {i}: {BuildingOperations.marked[i].X}x {BuildingOperations.marked[i].Y}y";
} }
} }
if (toPrint == "") if (toPrint == "")
MainClass.GetMonitor().Log("No positions marked!", LogLevel.Info); MainClass.InfoLog("No positions marked!");
else else
MainClass.GetMonitor().Log($"Marked positions:{toPrint}\nOpen command menu and use pageup and pagedown to check the list", LogLevel.Info); MainClass.InfoLog($"Marked positions:{toPrint}\nOpen command menu and use pageup and pagedown to check the list");
}); });
helper.ConsoleCommands.Add("buildlist", "List all buildings for selection for upgrading/demolishing/painting", (string commmand, string[] args) => helper.ConsoleCommands.Add("buildlist", "List all buildings for selection for upgrading/demolishing/painting", (string command, string[] args) =>
{ {
if ((Game1.activeClickableMenu is not CarpenterMenu && Game1.activeClickableMenu is not PurchaseAnimalsMenu) || !BuildingNAnimalMenuPatches.isOnFarm) onBuildListCalled();
{
MainClass.GetMonitor().Log($"Cannot list buildings.", LogLevel.Info);
return;
}
string toPrint = "";
Farm farm = (Farm)Game1.getLocationFromName("Farm");
Netcode.NetCollection<Building> buildings = farm.buildings;
int buildingIndex = 0;
for (int i = 0; i < buildings.Count; i++)
{
string? name = buildings[i].nameOfIndoorsWithoutUnique;
name = (name == "null") ? buildings[i].buildingType.Value : name;
BuildingNAnimalMenuPatches.availableBuildings[buildingIndex] = buildings[i];
toPrint = $"{toPrint}\nIndex {buildingIndex}: {name}: At {buildings[i].tileX}x and {buildings[i].tileY}y";
++buildingIndex;
}
if (toPrint == "")
{
MainClass.GetMonitor().Log("No appropriate buildings to list", LogLevel.Info);
}
else
{
MainClass.GetMonitor().Log($"Available buildings:{toPrint}\nOpen command menu and use pageup and pagedown to check the list", LogLevel.Info);
}
}); });
helper.ConsoleCommands.Add("buildsel", "Select the building index which you want to upgrade/demolish/paint", (string commmand, string[] args) => helper.ConsoleCommands.Add("buildsel", "Select the building index which you want to upgrade/demolish/paint", (string command, string[] args) =>
{ {
if ((Game1.activeClickableMenu is not CarpenterMenu && Game1.activeClickableMenu is not PurchaseAnimalsMenu) || !BuildingNAnimalMenuPatches.isOnFarm) if ((Game1.activeClickableMenu is not CarpenterMenu && Game1.activeClickableMenu is not PurchaseAnimalsMenu && Game1.activeClickableMenu is not AnimalQueryMenu) || (!CarpenterMenuPatch.isOnFarm && !PurchaseAnimalsMenuPatch.isOnFarm && !AnimalQueryMenuPatch.isOnFarm))
{ {
MainClass.GetMonitor().Log($"Cannot select building.", LogLevel.Info); MainClass.InfoLog($"Cannot select building.");
return; return;
} }
string? indexInString = args.ElementAtOrDefault(0); string? indexInString = args.ElementAtOrDefault(0);
if (indexInString == null) if (indexInString == null)
{ {
MainClass.GetMonitor().Log("Enter the index of the building too! Use buildlist", LogLevel.Info); MainClass.InfoLog("Enter the index of the building too! Use buildlist");
return; return;
} }
@@ -384,27 +374,27 @@ namespace stardew_access
if (!isParsable) if (!isParsable)
{ {
MainClass.GetMonitor().Log("Index can only be a number.", LogLevel.Info); MainClass.InfoLog("Index can only be a number.");
return; return;
} }
string? positionIndexInString = args.ElementAtOrDefault(1); string? positionIndexInString = args.ElementAtOrDefault(1);
int positionIndex = 0; int positionIndex = 0;
if (BuildingNAnimalMenuPatches.isMoving) if (CarpenterMenuPatch.isMoving)
{ {
if (BuildingNAnimalMenuPatches.isConstructing || BuildingNAnimalMenuPatches.isMoving) if (CarpenterMenuPatch.isConstructing || CarpenterMenuPatch.isMoving)
{ {
if (BuildingNAnimalMenuPatches.availableBuildings[index] == null) if (BuildingOperations.availableBuildings[index] == null)
{ {
MainClass.GetMonitor().Log($"No building found with index {index}. Use buildlist.", LogLevel.Info); MainClass.InfoLog($"No building found with index {index}. Use buildlist.");
return; return;
} }
if (positionIndexInString == null) if (positionIndexInString == null)
{ {
MainClass.GetMonitor().Log("Enter the index of marked place too! Use marklist.", LogLevel.Info); MainClass.InfoLog("Enter the index of marked place too! Use marklist.");
return; return;
} }
@@ -412,53 +402,136 @@ namespace stardew_access
if (!isParsable) if (!isParsable)
{ {
MainClass.GetMonitor().Log("Index can only be a number.", LogLevel.Info); MainClass.InfoLog("Index can only be a number.");
return; return;
} }
} }
} }
else if (BuildingNAnimalMenuPatches.isConstructing && !BuildingNAnimalMenuPatches.isUpgrading) else if (CarpenterMenuPatch.isConstructing && !CarpenterMenuPatch.isUpgrading)
{ {
if (BuildingNAnimalMenuPatches.marked[index] == Vector2.Zero) if (BuildingOperations.marked[index] == Vector2.Zero)
{ {
MainClass.GetMonitor().Log($"No marked position found at {index} index.", LogLevel.Info); MainClass.InfoLog($"No marked position found at {index} index.");
return; return;
} }
} }
else else
{ {
if (BuildingNAnimalMenuPatches.availableBuildings[index] == null) if (BuildingOperations.availableBuildings[index] == null)
{ {
MainClass.GetMonitor().Log($"No building found with index {index}. Use buildlist.", LogLevel.Info); MainClass.InfoLog($"No building found with index {index}. Use buildlist.");
return; return;
} }
} }
string? response = null; string? response = null;
if (Game1.activeClickableMenu is PurchaseAnimalsMenu) { BuildingNAnimalMenuPatches.PurchaseAnimal(BuildingNAnimalMenuPatches.availableBuildings[index]); } if (Game1.activeClickableMenu is PurchaseAnimalsMenu)
{
BuildingOperations.PurchaseAnimal(BuildingOperations.availableBuildings[index]);
}
else if (Game1.activeClickableMenu is AnimalQueryMenu)
{
BuildingOperations.MoveAnimal(BuildingOperations.availableBuildings[index]);
}
else else
{ {
if (BuildingNAnimalMenuPatches.isConstructing && !BuildingNAnimalMenuPatches.isUpgrading) { response = BuildingNAnimalMenuPatches.Contstruct(BuildingNAnimalMenuPatches.marked[index]); } if (CarpenterMenuPatch.isConstructing && !CarpenterMenuPatch.isUpgrading) { response = BuildingOperations.Contstruct(BuildingOperations.marked[index]); }
else if (BuildingNAnimalMenuPatches.isMoving) { response = BuildingNAnimalMenuPatches.Move(BuildingNAnimalMenuPatches.availableBuildings[index], BuildingNAnimalMenuPatches.marked[positionIndex]); } else if (CarpenterMenuPatch.isMoving) { response = BuildingOperations.Move(BuildingOperations.availableBuildings[index], BuildingOperations.marked[positionIndex]); }
else if (BuildingNAnimalMenuPatches.isDemolishing) { response = BuildingNAnimalMenuPatches.Demolish(BuildingNAnimalMenuPatches.availableBuildings[index]); } else if (CarpenterMenuPatch.isDemolishing) { response = BuildingOperations.Demolish(BuildingOperations.availableBuildings[index]); }
else if (BuildingNAnimalMenuPatches.isUpgrading) { response = BuildingNAnimalMenuPatches.Upgrade(BuildingNAnimalMenuPatches.availableBuildings[index]); } else if (CarpenterMenuPatch.isUpgrading) { response = BuildingOperations.Upgrade(BuildingOperations.availableBuildings[index]); }
else if (BuildingNAnimalMenuPatches.isPainting) { response = BuildingNAnimalMenuPatches.Paint(BuildingNAnimalMenuPatches.availableBuildings[index]); } else if (CarpenterMenuPatch.isPainting) { response = BuildingOperations.Paint(BuildingOperations.availableBuildings[index]); }
} }
if (response != null) if (response != null)
{ {
MainClass.GetMonitor().Log(response, LogLevel.Info); MainClass.InfoLog(response);
} }
}); });
#endregion #endregion
helper.ConsoleCommands.Add("refsr", "Refresh screen reader", (string commmand, string[] args) => #region Other
{ helper.ConsoleCommands.Add("refsr", "Refresh screen reader", (string command, string[] args) =>
MainClass.GetScreenReader().InitializeScreenReader(); {
MainClass.ScreenReader.InitializeScreenReader();
MainClass.GetMonitor().Log("Screen Reader refreshed!", LogLevel.Info); MainClass.InfoLog("Screen Reader refreshed!");
});
helper.ConsoleCommands.Add("refmc", "Refresh mod config", (string command, string[] args) =>
{
MainClass.Config = helper.ReadConfig<ModConfig>();
MainClass.InfoLog("Mod Config refreshed!");
}); });
helper.ConsoleCommands.Add("refst", "Refresh static tiles", (string command, string[] args) =>
{
StaticTiles.LoadTilesFiles();
StaticTiles.SetupTilesDicts();
MainClass.InfoLog("Static tiles refreshed!");
});
helper.ConsoleCommands.Add("hnspercent", "Toggle between speaking in percentage or full health and stamina.", (string command, string[] args) =>
{
MainClass.Config.HealthNStaminaInPercentage = !MainClass.Config.HealthNStaminaInPercentage;
helper.WriteConfig(MainClass.Config);
MainClass.InfoLog("Speaking in percentage is " + (MainClass.Config.HealthNStaminaInPercentage ? "on" : "off"));
});
helper.ConsoleCommands.Add("snapmouse", "Toggle snap mouse feature.", (string command, string[] args) =>
{
MainClass.Config.SnapMouse = !MainClass.Config.SnapMouse;
helper.WriteConfig(MainClass.Config);
MainClass.InfoLog("Snap Mouse is " + (MainClass.Config.SnapMouse ? "on" : "off"));
});
helper.ConsoleCommands.Add("warning", "Toggle warnings feature.", (string command, string[] args) =>
{
MainClass.Config.Warning = !MainClass.Config.Warning;
helper.WriteConfig(MainClass.Config);
MainClass.InfoLog("Warnings is " + (MainClass.Config.Warning ? "on" : "off"));
});
helper.ConsoleCommands.Add("tts", "Toggles the screen reader/tts", (string command, string[] args) =>
{
MainClass.Config.TTS = !MainClass.Config.TTS;
helper.WriteConfig(MainClass.Config);
MainClass.InfoLog("TTS is " + (MainClass.Config.TTS ? "on" : "off"));
});
#endregion
}
internal static void onBuildListCalled()
{
string toPrint = "";
Farm farm = (Farm)Game1.getLocationFromName("Farm");
Netcode.NetCollection<Building> buildings = farm.buildings;
int buildingIndex = 0;
for (int i = 0; i < buildings.Count; i++)
{
string? name = buildings[i].nameOfIndoorsWithoutUnique;
name = (name == "null") ? buildings[i].buildingType.Value : name;
BuildingOperations.availableBuildings[buildingIndex] = buildings[i];
toPrint = $"{toPrint}\nIndex {buildingIndex}: {name}: At {buildings[i].tileX}x and {buildings[i].tileY}y";
++buildingIndex;
}
if (toPrint == "")
{
MainClass.InfoLog("No appropriate buildings to list");
}
else
{
MainClass.InfoLog($"Available buildings:{toPrint}\nOpen command menu and use pageup and pagedown to check the list");
}
} }
} }
} }

View File

@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Audio;
using StardewModdingAPI;
using StardewValley; using StardewValley;
namespace stardew_access namespace stardew_access
@@ -16,10 +15,14 @@ namespace stardew_access
{ {
try try
{ {
if (MainClass.ModHelper == null)
return;
Dictionary<String, TYPE> soundEffects = new Dictionary<String, TYPE>(); Dictionary<String, TYPE> soundEffects = new Dictionary<String, TYPE>();
soundEffects.Add("drop_item", TYPE.Sound); soundEffects.Add("drop_item", TYPE.Sound);
soundEffects.Add("colliding", TYPE.Sound); soundEffects.Add("colliding", TYPE.Sound);
soundEffects.Add("invalid-selection", TYPE.Sound);
soundEffects.Add("npc_top", TYPE.Footstep); soundEffects.Add("npc_top", TYPE.Footstep);
soundEffects.Add("npc_right", TYPE.Footstep); soundEffects.Add("npc_right", TYPE.Footstep);
@@ -55,7 +58,7 @@ namespace stardew_access
} }
SoundEffect effect; SoundEffect effect;
string filePath = Path.Combine(MainClass.ModHelper.DirectoryPath, "sounds", $"{soundEffect.Key}.wav"); string filePath = Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "sounds", $"{soundEffect.Key}.wav");
using (FileStream stream = new(filePath, FileMode.Open)) using (FileStream stream = new(filePath, FileMode.Open))
{ {
effect = SoundEffect.FromStream(stream); effect = SoundEffect.FromStream(stream);
@@ -71,7 +74,7 @@ namespace stardew_access
} }
catch (Exception e) catch (Exception e)
{ {
MainClass.GetMonitor().Log($"Unable to initialize custom sounds:\n{e.Message}\n{e.StackTrace}", LogLevel.Error); MainClass.ErrorLog($"Unable to initialize custom sounds:\n{e.Message}\n{e.StackTrace}");
} }
} }
} }

View File

@@ -0,0 +1,366 @@
using Microsoft.Xna.Framework;
using stardew_access.Patches;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.Objects;
namespace stardew_access.Features
{
internal class BuildingOperations
{
internal static Building?[] availableBuildings = new Building[100];
internal static Vector2[] marked = new Vector2[10];
public static string? Demolish(Building? toDemolish)
{
if (toDemolish == null)
return null;
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::654)
Farm farm = (Farm)Game1.getLocationFromName("Farm");
Action buildingLockFailed = delegate
{
if (CarpenterMenuPatch.isDemolishing)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_LockFailed");
}
};
Action continueDemolish = delegate
{
if (CarpenterMenuPatch.isDemolishing && toDemolish != null && farm.buildings.Contains(toDemolish))
{
if ((int)toDemolish.daysOfConstructionLeft.Value > 0 || (int)toDemolish.daysUntilUpgrade.Value > 0)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_DuringConstruction");
}
else if (toDemolish.indoors.Value != null && toDemolish.indoors.Value is AnimalHouse && ((AnimalHouse)toDemolish.indoors.Value).animalsThatLiveHere.Count > 0)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_AnimalsHere");
}
else if (toDemolish.indoors.Value != null && toDemolish.indoors.Value.farmers.Any())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_PlayerHere");
}
else
{
if (toDemolish.indoors.Value != null && toDemolish.indoors.Value is Cabin)
{
foreach (Farmer current in Game1.getAllFarmers())
{
if (current.currentLocation != null && current.currentLocation.Name == ((Cabin)toDemolish.indoors.Value).GetCellarName())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_PlayerHere");
return;
}
}
}
if (toDemolish.indoors.Value is Cabin && ((Cabin)toDemolish.indoors.Value).farmhand.Value.isActive())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_FarmhandOnline");
}
else
{
toDemolish.BeforeDemolish();
Chest? chest = null;
if (toDemolish.indoors.Value is Cabin)
{
List<Item> list = ((Cabin)toDemolish.indoors.Value).demolish();
if (list.Count > 0)
{
chest = new Chest(playerChest: true);
chest.fixLidFrame();
chest.items.Set(list);
}
}
if (farm.destroyStructure(toDemolish))
{
_ = (int)toDemolish.tileY.Value;
_ = (int)toDemolish.tilesHigh.Value;
Game1.flashAlpha = 1f;
toDemolish.showDestroyedAnimation(Game1.getFarm());
Game1.playSound("explosion");
Utility.spreadAnimalsAround(toDemolish, farm);
if (CarpenterMenuPatch.carpenterMenu != null)
DelayedAction.functionAfterDelay(CarpenterMenuPatch.carpenterMenu.returnToCarpentryMenu, 1500);
// freeze = true;
if (chest != null)
{
farm.objects[new Vector2((int)toDemolish.tileX.Value + (int)toDemolish.tilesWide.Value / 2, (int)toDemolish.tileY.Value + (int)toDemolish.tilesHigh.Value / 2)] = chest;
}
}
}
}
}
};
if (toDemolish != null)
{
if (toDemolish.indoors.Value != null && toDemolish.indoors.Value is Cabin && !Game1.IsMasterGame)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_LockFailed");
toDemolish = null;
return response;
}
if (CarpenterMenuPatch.carpenterMenu != null && !CarpenterMenuPatch.carpenterMenu.CanDemolishThis(toDemolish))
{
toDemolish = null;
return response;
}
if (CarpenterMenuPatch.carpenterMenu != null && !Game1.IsMasterGame && !CarpenterMenuPatch.carpenterMenu.hasPermissionsToDemolish(toDemolish))
{
toDemolish = null;
return response;
}
}
if (toDemolish != null && toDemolish.indoors.Value is Cabin)
{
Cabin cabin = (Cabin)toDemolish.indoors.Value;
if (cabin.farmhand.Value != null && (bool)cabin.farmhand.Value.isCustomized.Value)
{
Game1.currentLocation.createQuestionDialogue(Game1.content.LoadString("Strings\\UI:Carpenter_DemolishCabinConfirm", cabin.farmhand.Value.Name), Game1.currentLocation.createYesNoResponses(), delegate (Farmer f, string answer)
{
if (answer == "Yes")
{
Game1.activeClickableMenu = CarpenterMenuPatch.carpenterMenu;
Game1.player.team.demolishLock.RequestLock(continueDemolish, buildingLockFailed);
}
else
{
if (CarpenterMenuPatch.carpenterMenu != null)
DelayedAction.functionAfterDelay(CarpenterMenuPatch.carpenterMenu.returnToCarpentryMenu, 1000);
}
});
return response;
}
}
if (toDemolish != null)
{
Game1.player.team.demolishLock.RequestLock(continueDemolish, buildingLockFailed);
}
return response;
}
public static string? Contstruct(Vector2 position)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::874)
Game1.player.team.buildLock.RequestLock(delegate
{
if (CarpenterMenuPatch.isOnFarm && Game1.locationRequest == null)
{
if (tryToBuild(position))
{
if (CarpenterMenuPatch.carpenterMenu != null)
{
CarpenterMenuPatch.carpenterMenu.CurrentBlueprint.consumeResources();
DelayedAction.functionAfterDelay(CarpenterMenuPatch.carpenterMenu.returnToCarpentryMenuAfterSuccessfulBuild, 2000);
}
// freeze = true;
}
else
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantBuild");
}
}
Game1.player.team.buildLock.ReleaseLock();
});
return response;
}
public static bool tryToBuild(Vector2 position)
{
if (CarpenterMenuPatch.carpenterMenu == null)
return false;
return ((Farm)Game1.getLocationFromName("Farm")).buildStructure(CarpenterMenuPatch.carpenterMenu.CurrentBlueprint, position, Game1.player, CarpenterMenuPatch.isMagicalConstruction);
}
public static string? Upgrade(Building? toUpgrade)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::775)
if (CarpenterMenuPatch.carpenterMenu != null && toUpgrade != null && CarpenterMenuPatch.carpenterMenu.CurrentBlueprint.name != null && toUpgrade.buildingType.Equals(CarpenterMenuPatch.carpenterMenu.CurrentBlueprint.nameOfBuildingToUpgrade))
{
CarpenterMenuPatch.carpenterMenu.CurrentBlueprint.consumeResources();
toUpgrade.daysUntilUpgrade.Value = 2;
toUpgrade.showUpgradeAnimation(Game1.getFarm());
Game1.playSound("axe");
DelayedAction.functionAfterDelay(CarpenterMenuPatch.carpenterMenu.returnToCarpentryMenuAfterSuccessfulBuild, 1500);
// freeze = true;
// Game1.multiplayer.globalChatInfoMessage("BuildingBuild", Game1.player.Name, Utility.AOrAn(carpenterMenu.CurrentBlueprint.displayName), carpenterMenu.CurrentBlueprint.displayName, Game1.player.farmName.Value);
}
else if (toUpgrade != null)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantUpgrade_BuildingType");
}
return response;
}
public static string? Paint(Building? toPaint)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::793)
Farm farm_location = Game1.getFarm();
if (toPaint != null)
{
if (!toPaint.CanBePainted())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint");
return response;
}
if (CarpenterMenuPatch.carpenterMenu != null && !CarpenterMenuPatch.carpenterMenu.HasPermissionsToPaint(toPaint))
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint_Permission");
return response;
}
toPaint.color.Value = Color.White;
if (CarpenterMenuPatch.carpenterMenu != null)
CarpenterMenuPatch.carpenterMenu.SetChildMenu(new StardewValley.Menus.BuildingPaintMenu(toPaint));
}
/* TODO Add painting of farm house
else if (farm_location.GetHouseRect().Contains(Utility.Vector2ToPoint(new Vector2(toPaint.tileX, toPaint.tileY))))
{
if (!carpenterMenu.CanPaintHouse())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint");
}
else if (!carpenterMenu.HasPermissionsToPaint(null))
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint_Permission");
}
else
{
carpenterMenu.SetChildMenu(new BuildingPaintMenu("House", () => (farm_location.paintedHouseTexture != null) ? farm_location.paintedHouseTexture : Farm.houseTextures, farm_location.houseSource.Value, farm_location.housePaintColor.Value));
}
}*/
return response;
}
public static string? Move(Building? buildingToMove, Vector2 position)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::829)
if (buildingToMove != null)
{
string? name = buildingToMove.nameOfIndoorsWithoutUnique;
name = (name == "null") ? buildingToMove.buildingType.Value : name;
if ((int)buildingToMove.daysOfConstructionLeft.Value > 0)
{
buildingToMove = null;
return "Building under construction, cannot move";
}
if (CarpenterMenuPatch.carpenterMenu != null && !CarpenterMenuPatch.carpenterMenu.hasPermissionsToMove(buildingToMove))
{
buildingToMove = null;
return "You don't have permission to move this building";
}
Game1.playSound("axchop");
if (((Farm)Game1.getLocationFromName("Farm")).buildStructure(buildingToMove, position, Game1.player))
{
if (buildingToMove is ShippingBin)
{
((ShippingBin)buildingToMove).initLid();
}
if (buildingToMove is GreenhouseBuilding)
{
Game1.getFarm().greenhouseMoved.Value = true;
}
buildingToMove.performActionOnBuildingPlacement();
buildingToMove = null;
Game1.playSound("axchop");
DelayedAction.playSoundAfterDelay("dirtyHit", 50);
DelayedAction.playSoundAfterDelay("dirtyHit", 150);
response = $"{buildingToMove} moved to {position.X}x {position.Y}y";
}
else
{
Game1.playSound("cancel");
response = $"Cannot move building to {position.X}x {position.Y}y";
}
}
return response;
}
public static void PurchaseAnimal(Building? selection)
{
if (selection == null)
return;
if (PurchaseAnimalsMenuPatch.purchaseAnimalsMenu == null)
return;
int x = (selection.tileX.Value * Game1.tileSize) - Game1.viewport.X;
int y = (selection.tileY.Value * Game1.tileSize) - Game1.viewport.Y;
if (PurchaseAnimalsMenuPatch.animalBeingPurchased != null && !selection.buildingType.Value.Contains(PurchaseAnimalsMenuPatch.animalBeingPurchased.buildingTypeILiveIn.Value))
{
string warn = Game1.content.LoadString("Strings\\StringsFromCSFiles:PurchaseAnimalsMenu.cs.11326", PurchaseAnimalsMenuPatch.animalBeingPurchased.displayType);
MainClass.ScreenReader.Say(warn, true);
return;
}
if (((AnimalHouse)selection.indoors.Value).isFull())
{
string warn = Game1.content.LoadString("Strings\\StringsFromCSFiles:PurchaseAnimalsMenu.cs.11321");
MainClass.ScreenReader.Say(warn, true);
return;
}
PurchaseAnimalsMenuPatch.purchaseAnimalsMenu.receiveLeftClick(x, y);
}
public static void MoveAnimal(Building? selection)
{
if (selection == null)
return;
if (AnimalQueryMenuPatch.animalQueryMenu == null)
return;
if (AnimalQueryMenuPatch.animalBeingMoved == null)
return;
// The following code is taken from the game's source code [AnimalQueryMenu.cs::receiveLeftClick]
if (selection.buildingType.Value.Contains(AnimalQueryMenuPatch.animalBeingMoved.buildingTypeILiveIn.Value))
{
if (((AnimalHouse)selection.indoors.Value).isFull())
{
string warn = Game1.content.LoadString("Strings\\UI:AnimalQuery_Moving_BuildingFull");
MainClass.ScreenReader.Say(warn, true);
return;
}
if (selection.Equals(AnimalQueryMenuPatch.animalBeingMoved.home))
{
string warn = Game1.content.LoadString("Strings\\UI:AnimalQuery_Moving_AlreadyHome");
MainClass.ScreenReader.Say(warn, true);
return;
}
((AnimalHouse)AnimalQueryMenuPatch.animalBeingMoved.home.indoors.Value).animalsThatLiveHere.Remove(AnimalQueryMenuPatch.animalBeingMoved.myID.Value);
if (((AnimalHouse)AnimalQueryMenuPatch.animalBeingMoved.home.indoors.Value).animals.ContainsKey(AnimalQueryMenuPatch.animalBeingMoved.myID.Value))
{
((AnimalHouse)selection.indoors.Value).animals.Add(AnimalQueryMenuPatch.animalBeingMoved.myID.Value, AnimalQueryMenuPatch.animalBeingMoved);
((AnimalHouse)AnimalQueryMenuPatch.animalBeingMoved.home.indoors.Value).animals.Remove(AnimalQueryMenuPatch.animalBeingMoved.myID.Value);
}
AnimalQueryMenuPatch.animalBeingMoved.home = selection;
AnimalQueryMenuPatch.animalBeingMoved.homeLocation.Value = new Vector2((int)selection.tileX.Value, (int)selection.tileY.Value);
((AnimalHouse)selection.indoors.Value).animalsThatLiveHere.Add(AnimalQueryMenuPatch.animalBeingMoved.myID.Value);
AnimalQueryMenuPatch.animalBeingMoved.makeSound();
Game1.globalFadeToBlack(AnimalQueryMenuPatch.animalQueryMenu.finishedPlacingAnimal);
}
else
{
string warn = Game1.content.LoadString("Strings\\UI:AnimalQuery_Moving_CantLiveThere");
MainClass.ScreenReader.Say(warn, true);
}
return;
}
}
}

View File

@@ -6,115 +6,185 @@ namespace stardew_access.Features
internal class CurrentPlayer internal class CurrentPlayer
{ {
public static int getHealth() /// <summary>
/// Returns the percentage health remaining of player.
/// </summary>
public static int PercentHealth
{ {
if (Game1.player == null) get
return 0;
int maxHealth = Game1.player.maxHealth;
int currentHealth = Game1.player.health;
int healthPercentage = (int)(currentHealth * 100) / maxHealth;
return healthPercentage;
}
public static int getStamina()
{
if (Game1.player == null)
return 0;
int maxStamina = Game1.player.maxStamina;
int currentStamine = (int)Game1.player.stamina;
int staminaPercentage = (int)(currentStamine * 100) / maxStamina;
return staminaPercentage;
}
public static int getPositionX()
{
if (Game1.player == null)
return 0;
int x = (int)Game1.player.getTileLocation().X;
return x;
}
public static int getPositionY()
{
if (Game1.player == null)
return 0;
int y = (int)Game1.player.getTileLocation().Y;
return y;
}
public static string getTimeOfDay()
{
int timeOfDay = Game1.timeOfDay;
int minutes = timeOfDay % 100;
int hours = timeOfDay / 100;
string amOrpm = "A M";
if (hours >= 12)
{ {
amOrpm = "P M"; if (Game1.player == null)
if (hours > 12) return 0;
hours -= 12;
return (CurrentHealth * 100) / Game1.player.maxHealth; ;
} }
return $"{hours}:{minutes} {amOrpm}";
} }
public static string getSeason() /// <summary>
/// Returns the total health player has currently
/// </summary>
public static int CurrentHealth
{ {
return Game1.CurrentSeasonDisplayName; get
}
public static int getDate()
{
return Game1.dayOfMonth;
}
public static string getDay()
{
return Game1.Date.DayOfWeek.ToString();
}
public static int getMoney()
{
if (Game1.player == null)
return -1;
return Game1.player.Money;
}
public static Vector2 getNextTile()
{
int x = Game1.player.GetBoundingBox().Center.X;
int y = Game1.player.GetBoundingBox().Center.Y;
int offset = 64;
switch (Game1.player.FacingDirection)
{ {
case 0: if (Game1.player == null)
y -= offset; return 0;
break;
case 1:
x += offset;
break;
case 2:
y += offset;
break;
case 3:
x -= offset;
break;
}
x /= Game1.tileSize; return Game1.player.health;
y /= Game1.tileSize; }
return new Vector2(x, y); }
/// <summary>
/// Returns the percentage stamine/energy remaining of player.
/// </summary>
public static int PercentStamina
{
get
{
if (Game1.player == null)
return 0;
return (CurrentStamina * 100) / Game1.player.maxStamina.Value;
}
}
/// <summary>
/// Returns the total stamina player has currently
/// </summary>
public static int CurrentStamina
{
get
{
if (Game1.player == null)
return 0;
return (int)Game1.player.stamina;
}
}
/// <summary>
/// Returns the tile location of the player
/// </summary>
public static Vector2 Position
{
get
{
if (Game1.player == null)
return Vector2.Zero;
return Game1.player.getTileLocation();
}
}
/// <summary>
/// Returns the X coordinate of the player
/// </summary>
public static int PositionX
{
get
{
if (Game1.player == null)
return 0;
return (int)Position.X;
}
}
/// <summary>
/// Returns the Y coordinate of the player
/// </summary>
public static int PositionY
{
get
{
if (Game1.player == null)
return 0;
return (int)Position.Y;
}
}
/// <summary>
/// Returns the time in the 12 hours format
/// </summary>
public static string TimeOfDay
{
get
{
int timeOfDay = Game1.timeOfDay;
int minutes = timeOfDay % 100;
int hours = timeOfDay / 100;
string amOrpm = hours / 12 == 1 ? "PM" : "AM";
hours = hours % 12;
if (hours == 0) hours = 12;
return $"{hours}:{minutes:00} {amOrpm}";
}
}
/// <summary>
/// Returns the current season
/// </summary>
public static string Season => Game1.CurrentSeasonDisplayName;
/// <summary>
/// Returns the current date of month
/// </summary>
public static int Date => Game1.dayOfMonth;
/// <summary>
/// Returns the current day of week
/// </summary>
/// <returns></returns>
public static string Day => Game1.Date.DayOfWeek.ToString();
/// <summary>
/// Returns the amount of money the player has currently
/// </summary>
public static int Money
{
get
{
if (Game1.player == null)
return -1;
return Game1.player.Money;
}
}
/// <summary>
/// Returns the tile position of the tile the player is facing
/// </summary>
/// <value></value>
public static Vector2 FacingTile
{
get
{
int x = Game1.player.GetBoundingBox().Center.X;
int y = Game1.player.GetBoundingBox().Center.Y;
int offset = 64;
switch (Game1.player.FacingDirection)
{
case 0:
y -= offset;
break;
case 1:
x += offset;
break;
case 2:
y += offset;
break;
case 3:
x -= offset;
break;
}
x /= Game1.tileSize;
y /= Game1.tileSize;
return new Vector2(x, y);
}
} }
} }
} }

View File

@@ -0,0 +1,807 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using static stardew_access.Features.Utils;
using System.Text.Json;
namespace stardew_access.Features
{
/// <summary>
/// Provides methods to locate tiles of interest in various game locations that are conditional or unpredictable (I.E. not static).
/// </summary>
/// <remarks>
/// The DynamicTiles class currently supports the following location types:
/// - Beach
/// - BoatTunnel
/// - CommunityCenter
/// - Farm
/// - FarmHouse
/// - Forest
/// - IslandFarmHouse
/// - IslandLocation
/// - LibraryMuseum
/// - Town
///
/// And the following Island LocationTypes:
/// - IslandNorth
/// - IslandWest
/// - VolcanoDungeon
///
/// The class also supports the following named locations:
/// - Barn (and its upgraded versions)
/// - Coop (and its upgraded versions)
///
/// The class does not yet support the following location types, but consider adding support in future updates:
/// - AbandonedJojaMart
/// - AdventureGuild
/// - BathHousePool
/// - BeachNightMarket
/// - BugLand
/// - BusStop
/// - Caldera
/// - Cellar
/// - Club
/// - Desert
/// - FarmCave
/// - FishShop
/// - JojaMart
/// - ManorHouse
/// - MermaidHouse
/// - Mine
/// - Mountain
/// - MovieTheater
/// - Railroad
/// - SeedShop
/// - Sewer
/// - Submarine
/// - Summit
/// - WizardHouse
/// - Woods
///
/// The class does not yet support the following named locations, but consider adding support in future updates:
/// - "AnimalShop"
/// - "Backwoods"
/// - "BathHouse_Entry"
/// - "BathHouse_MensLocker"
/// - "BathHouse_WomensLocker"
/// - "Blacksmith"
/// - "ElliottHouse"
/// - "FarmGreenHouse"
/// - "Greenhouse"
/// - "HaleyHouse"
/// - "HarveyRoom"
/// - "Hospital"
/// - "JoshHouse"
/// - "LeahHouse"
/// - "LeoTreeHouse"
/// - "Saloon"
/// - "SamHouse"
/// - "SandyHouse"
/// - "ScienceHouse"
/// - "SebastianRoom"
/// - "SkullCave"
/// - "Sunroom"
/// - "Tent"
/// - "Trailer"
/// - "Trailer_Big"
/// - "Tunnel"
/// - "WitchHut"
/// - "WitchSwamp"
/// - "WitchWarpCave"
/// - "WizardHouseBasement"
///
/// The class does not yet support the following IslandLocation location types, but consider adding support in future updates:
/// - IslandEast
/// - IslandFarmCave
/// - IslandFieldOffice
/// - IslandHut
/// - IslandShrine
/// - IslandSouth
/// - IslandSouthEast
/// - IslandSouthEastCave
/// - IslandWestCave1
///
/// The class does not yet support the following IslandLocation named locations, but consider adding support in future updates:
/// - "CaptainRoom"
/// - "IslandNorthCave1"
/// - "QiNutRoom"
/// </remarks>
public class DynamicTiles
{
// Static instance for the singleton pattern
private static DynamicTiles? _instance;
/// <summary>
/// The singleton instance of the <see cref="DynamicTiles"/> class.
/// </summary>
public static DynamicTiles Instance
{
get
{
_instance ??= new DynamicTiles();
return _instance;
}
}
// HashSet for storing which unimplemented locations have been previously logged
private static readonly HashSet<object> loggedLocations = new();
// Dictionary of coordinates for feeding benches in barns and coops
private static readonly Dictionary<string, (int minX, int maxX, int y)> FeedingBenchBounds = new()
{
{ "Barn", (8, 11, 3) },
{ "Barn2", (8, 15, 3) },
{ "Big Barn", (8, 15, 3) },
{ "Barn3", (8, 19, 3) },
{ "Deluxe Barn", (8, 19, 3) },
{ "Coop", (6, 9, 3) },
{ "Coop2", (6, 13, 3) },
{ "Big Coop", (6, 13, 3) },
{ "Coop3", (6, 17, 3) },
{ "Deluxe Coop", (6, 17, 3) }
};
// Dictionary to hold event info
private static readonly Dictionary<string, Dictionary<(int X, int Y), string>> EventInteractables;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicTiles"/> class.
/// Loads the event file.
/// </summary>
static DynamicTiles()
{
EventInteractables = LoadEventTiles();
}
/// <summary>
/// Loads event tiles from the "event-tiles.json" file and returns a dictionary representation of the data.
/// </summary>
/// <returns>
/// A dictionary with event names as keys and nested dictionaries as values, where nested dictionaries have
/// coordinate tuples (x, y) as keys and tile names as values.
/// </returns>
private static Dictionary<string, Dictionary<(int x, int y), string>> LoadEventTiles()
{
JsonElement json = LoadJsonFile("event-tiles.json");
if (json.ValueKind == JsonValueKind.Undefined)
{
// If the JSON couldn't be loaded or parsed, return an empty dictionary
return new Dictionary<string, Dictionary<(int x, int y), string>>();
}
var eventTiles = new Dictionary<string, Dictionary<(int x, int y), string>>();
// Iterate over the JSON properties to create a dictionary representation of the data
foreach (JsonProperty eventProperty in json.EnumerateObject())
{
string eventName = eventProperty.Name;
var coordinates = new Dictionary<(int x, int y), string>();
// Iterate over the coordinate properties to create a nested dictionary with coordinate tuples as keys
foreach (JsonProperty coordinateProperty in eventProperty.Value.EnumerateObject())
{
string[] xy = coordinateProperty.Name.Split(',');
int x = int.Parse(xy[0]);
int y = int.Parse(xy[1]);
coordinates.Add((x, y), value: coordinateProperty.Value.GetString() ?? string.Empty);
}
eventTiles.Add(eventName, coordinates);
}
return eventTiles;
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in a Beach.
/// </summary>
/// <param name="beach">The Beach to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetBeachInfo(Beach beach, int x, int y, bool lessInfo = false)
{
if (MainClass.ModHelper == null)
{
return (null, null);
}
if (MainClass.ModHelper.Reflection.GetField<NPC>(beach, "oldMariner").GetValue() is NPC mariner && mariner.getTileLocation() == new Vector2(x, y))
{
return ("Old Mariner", CATEGORY.NPCs);
}
else if (x == 58 && y == 13)
{
if (!beach.bridgeFixed.Value)
{
return ("Repair Bridge", CATEGORY.Interactables);
}
else
{
return ("Bridge", CATEGORY.Bridges);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a BoatTunnel.
/// </summary>
/// <param name="boatTunnel">The BoatTunnel to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetBoatTunnelInfo(BoatTunnel boatTunnel, int x, int y, bool lessInfo = false)
{
// Check if the player has received the specified mail or not
bool HasMail(string mail) => Game1.MasterPlayer.hasOrWillReceiveMail(mail);
// If the position matches one of the interactable elements in the boat tunnel
if ((x, y) == (4, 9) || (x, y) == (6, 8) || (x, y) == (8, 9))
{
string mail = (x, y) switch
{
(4, 9) => "willyBoatFixed",
(6, 8) => "willyBoatHull",
(8, 9) => "willyBoatAnchor",
_ => throw new InvalidOperationException("Unexpected (x, y) values"),
};
string itemName = (x, y) switch
{
(4, 9) => "Ticket Machine",
(6, 8) => "Boat Hull",
(8, 9) => "Boat Anchor",
_ => throw new InvalidOperationException("Unexpected (x, y) values"),
};
CATEGORY category = (x, y) == (4, 9) ? CATEGORY.Interactables : (!HasMail(mail) ? CATEGORY.Interactables : CATEGORY.Decor);
return ((!HasMail(mail) ? "Repair " : "") + itemName, category);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a CommunityCenter.
/// </summary>
/// <param name="communityCenter">The CommunityCenter to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetCommunityCenterInfo(CommunityCenter communityCenter, int x, int y, bool lessInfo = false)
{
if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10)
{
return ("Missed Rewards Chest", CATEGORY.Containers);
}
return (null, null);
}
/// <summary>
/// Gets the building information for a given position on a farm.
/// </summary>
/// <param name="building">The Building instance.</param>
/// <param name="x">The x-coordinate of the position.</param>
/// <param name="y">The y-coordinate of the position.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the door or building found, or (null, null) if no door or building is found.</returns>
private static (string? name, CATEGORY? category) GetBuildingInfo(Building building, int x, int y, bool lessInfo = false)
{
string name = building.buildingType.Value;
int buildingTileX = building.tileX.Value;
int buildingTileY = building.tileY.Value;
// If the building is a FishPond, prepend the fish name
if (building is FishPond fishPond && fishPond.fishType.Value >= 0)
{
name = $"{Game1.objectInformation[fishPond.fishType.Value].Split('/')[4]} {name}";
}
// Calculate differences in x and y coordinates
int offsetX = x - buildingTileX;
int offsetY = y - buildingTileY;
// Check if the position matches the human door
if (building.humanDoor.Value.X == offsetX && building.humanDoor.Value.Y == offsetY)
{
return (name + " Door", CATEGORY.Doors);
}
// Check if the position matches the animal door
else if (building.animalDoor.Value.X == offsetX && building.animalDoor.Value.Y == offsetY)
{
return (name + " Animal Door " + ((building.animalDoorOpen.Value) ? "Opened" : "Closed"), CATEGORY.Doors);
}
// Check if the position matches the building's top-left corner
else if (offsetX == 0 && offsetY == 0)
{
return (name, CATEGORY.Buildings);
}
// Special handling for Mill buildings
else if (building is Mill)
{
// Check if the position matches the input
if (offsetX == 1 && offsetY == 1)
{
return (name + " input", CATEGORY.Buildings);
}
// Check if the position matches the output
else if (offsetX == 3 && offsetY == 1)
{
return (name + " output", CATEGORY.Buildings);
}
}
// Return the building name for any other position within the building's area
return (name, CATEGORY.Buildings);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a Farm.
/// </summary>
/// <param name="farm">The Farm to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetFarmInfo(Farm farm, int x, int y, bool lessInfo = false)
{
var mainMailboxPos = farm.GetMainMailboxPosition();
Building building = farm.getBuildingAt(new Vector2(x, y));
if (mainMailboxPos.X == x && mainMailboxPos.Y == y)
{
return ("Mail box", CATEGORY.Interactables);
}
else if (building is not null) // Check if there is a building at the current position
{
return GetBuildingInfo(building, x, y, lessInfo);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a FarmHouse.
/// </summary>
/// <param name="farmHouse">The FarmHouse to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetFarmHouseInfo(FarmHouse farmHouse, int x, int y, bool lessInfo = false)
{
if (farmHouse.upgradeLevel >= 1)
{
int kitchenX = farmHouse.getKitchenStandingSpot().X;
int kitchenY = farmHouse.getKitchenStandingSpot().Y - 1;
if (kitchenX == x && kitchenY == y)
{
return ("Stove", CATEGORY.Interactables);
}
else if (kitchenX + 1 == x && kitchenY == y)
{
return ("Sink", CATEGORY.Others);
}
else if (farmHouse.fridgePosition.X == x && farmHouse.fridgePosition.Y == y)
{
return ("Fridge", CATEGORY.Interactables);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a Forest.
/// </summary>
/// <param name="forest">The Forest to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetForestInfo(Forest forest, int x, int y, bool lessInfo = false)
{
if (forest.travelingMerchantDay && x == 27 && y == 11)
{
return ("Travelling Cart", CATEGORY.Interactables);
}
else if (forest.log != null && x == 2 && y == 7)
{
return ("Log", CATEGORY.Interactables);
}
else if (forest.log == null && x == 0 && y == 7)
{
return ("Secret Woods Entrance", CATEGORY.Doors);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandFarmHouse.
/// </summary>
/// <param name="islandFarmHouse">The IslandFarmHouse to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandFarmHouseInfo(IslandFarmHouse islandFarmHouse, int x, int y, bool lessInfo = false)
{
int fridgeX = islandFarmHouse.fridgePosition.X;
int fridgeY = islandFarmHouse.fridgePosition.Y;
if (fridgeX - 2 == x && fridgeY == y)
{
return ("Stove", CATEGORY.Interactables);
}
else if (fridgeX - 1 == x && fridgeY == y)
{
return ("Sink", CATEGORY.Others);
}
else if (fridgeX == x && fridgeY == y)
{
return ("Fridge", CATEGORY.Interactables);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandNorth.
/// </summary>
/// <param name="islandNorth">The IslandNorth to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandNorthInfo(IslandNorth islandNorth, int x, int y, bool lessInfo = false)
{
// Check if the trader is activated and the coordinates match the trader's location
if (islandNorth.traderActivated.Value && x == 36 && y == 71)
{
return ("Island Trader", CATEGORY.Interactables);
}
// Return (null, null) if no relevant object is found
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandWest.
/// </summary>
/// <param name="islandWest">The IslandWest to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandWestInfo(IslandWest islandWest, int x, int y, bool lessInfo = false)
{
// Check if the coordinates match the shipping bin's location
if ((islandWest.shippingBinPosition.X == x || (islandWest.shippingBinPosition.X + 1) == x) && islandWest.shippingBinPosition.Y == y)
{
return ("Shipping Bin", CATEGORY.Interactables);
}
// Return (null, null) if no relevant object is found
return (null, null);
}
/// <summary>
/// Retrieves information about tiles at a given coordinate in a VolcanoDungeon.
/// </summary>
/// <param name="dungeon">The VolcanoDungeon to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name of the tile and the CATEGORY, or (null, null) if no relevant tile is found.</returns>
private static (string? name, CATEGORY? category) GetVolcanoDungeonInfo(VolcanoDungeon dungeon, int x, int y, bool lessInfo = false)
{
if (!lessInfo)
{
if (dungeon.IsCooledLava(x, y))
{
return ("Cooled lava", CATEGORY.WaterTiles);
}
else if (StardewValley.Monsters.LavaLurk.IsLavaTile(dungeon, x, y))
{
return ("Lava", CATEGORY.WaterTiles);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in a named IslandLocation.
/// </summary>
/// <param name="islandLocation">The named IslandLocation to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetNamedIslandLocationInfo(IslandLocation islandLocation, int x, int y, bool lessInfo = false)
{
object locationType = islandLocation is not null and IslandLocation ? islandLocation.Name ?? "Undefined Island Location" : islandLocation!.GetType();
// Implement specific logic for named IslandLocations here, if necessary
// Unimplemented locations are logged.
// Check if the location has already been logged
if (!loggedLocations.Contains(locationType))
{
// Log the message
MainClass.DebugLog($"Called GetNamedIslandLocationInfo with unimplemented IslandLocation of type {islandLocation.GetType()} and name {islandLocation.Name}");
// Add the location to the HashSet to prevent logging it again
loggedLocations.Add(locationType);
}
return (null, null);
}
/// <summary>
/// Retrieves the name of the IslandGemBird based on its item index value.
/// </summary>
/// <param name="bird">The IslandGemBird instance.</param>
/// <returns>A string representing the name of the IslandGemBird.</returns>
private static String GetGemBirdName(IslandGemBird bird)
{
// Use a switch expression to return the appropriate bird name based on the item index value
return bird.itemIndex.Value switch
{
60 => "Emerald Gem Bird",
62 => "Aquamarine Gem Bird",
64 => "Ruby Gem Bird",
66 => "Amethyst Gem Bird",
68 => "Topaz Gem Bird",
_ => "Gem Bird", // Default case for when the item index does not match any of the specified values
};
}
/// <summary>
/// Gets the parrot perch information at the specified tile coordinates in the given island location.
/// </summary>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <param name="islandLocation">The IslandLocation where the parrot perch might be found.</param>
/// <returns>A string containing the parrot perch information if a parrot perch is found at the specified tile; null if no parrot perch is found.</returns>
private static string? GetParrotPerchAtTile(IslandLocation islandLocation, int x, int y)
{
// Use LINQ to find the first parrot perch at the specified tile (x, y) coordinates
var foundPerch = islandLocation.parrotUpgradePerches.FirstOrDefault(perch => perch.tilePosition.Value.Equals(new Point(x, y)));
// If a parrot perch was found at the specified tile coordinates
if (foundPerch != null)
{
string toSpeak = $"Parrot required nuts {foundPerch.requiredNuts.Value}";
// Return appropriate string based on the current state of the parrot perch
return foundPerch.currentState.Value switch
{
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Idle => foundPerch.IsAvailable() ? toSpeak : "Empty parrot perch",
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.StartBuilding => "Parrots started building request",
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Building => "Parrots building request",
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Complete => "Request Completed",
_ => toSpeak,
};
}
// If no parrot perch was found, return null
return null;
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandLocation.
/// </summary>
/// <param name="islandLocation">The IslandLocation to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandLocationInfo(IslandLocation islandLocation, int x, int y, bool lessInfo = false)
{
var nutTracker = Game1.player.team.collectedNutTracker;
string? parrot = GetParrotPerchAtTile(islandLocation, x, y);
if (islandLocation.IsBuriedNutLocation(new Point(x, y)) && !nutTracker.ContainsKey($"Buried_{islandLocation.Name}_{x}_{y}"))
{
return ("Diggable spot", CATEGORY.Interactables);
}
else if (islandLocation.locationGemBird.Value is IslandGemBird bird && ((int)bird.position.X / Game1.tileSize) == x && ((int)bird.position.Y / Game1.tileSize) == y)
{
return (GetGemBirdName(bird), CATEGORY.NPCs);
}
else if (parrot != null)
{
return (parrot, CATEGORY.Buildings);
}
return islandLocation switch
{
IslandNorth islandNorth => GetIslandNorthInfo(islandNorth, x, y, lessInfo),
IslandWest islandWest => GetIslandWestInfo(islandWest, x, y, lessInfo),
VolcanoDungeon dungeon => GetVolcanoDungeonInfo(dungeon, x, y, lessInfo),
_ => GetNamedIslandLocationInfo(islandLocation, x, y, lessInfo)
};
}
/// <summary>
/// Retrieves the value of the "Action" property from the Buildings layer tile at the given coordinates.
/// </summary>
/// <param name="libraryMuseum">The LibraryMuseum containing the tile.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>The value of the "Action" property as a string, or null if the property is not found.</returns>
private static string? GetTileActionPropertyValue(LibraryMuseum libraryMuseum, int x, int y, bool lessInfo = false)
{
xTile.Tiles.Tile tile = libraryMuseum.map.GetLayer("Buildings").PickTile(new xTile.Dimensions.Location(x * 64, y * 64), Game1.viewport.Size);
return tile.Properties.TryGetValue("Action", out xTile.ObjectModel.PropertyValue? value) ? value.ToString() : null;
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in a LibraryMuseum.
/// </summary>
/// <param name="libraryMuseum">The LibraryMuseum to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetLibraryMuseumInfo(LibraryMuseum libraryMuseum, int x, int y, bool lessInfo = false)
{
if (libraryMuseum.museumPieces.TryGetValue(new Vector2(x, y), out int museumPiece))
{
string displayName = Game1.objectInformation[museumPiece].Split('/')[0];
return ($"{displayName} showcase", CATEGORY.Interactables);
}
int booksFound = Game1.netWorldState.Value.LostBooksFound.Value;
string? action = libraryMuseum.doesTileHaveProperty(x, y, "Action", "Buildings");
if (action != null && action.Contains("Notes"))
{
string? actionPropertyValue = GetTileActionPropertyValue(libraryMuseum, x, y, lessInfo);
if (actionPropertyValue != null)
{
int which = Convert.ToInt32(actionPropertyValue.Split(' ')[1]);
if (booksFound >= which)
{
string message = Game1.content.LoadString("Strings\\Notes:" + which);
return ($"{message.Split('\n')[0]} Book", CATEGORY.Interactables);
}
return ($"Lost Book", CATEGORY.Others);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a Town.
/// </summary>
/// <param name="town">The Town to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetTownInfo(Town town, int x, int y, bool lessInfo = false)
{
if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93)
{
return ("Special quest board", CATEGORY.Interactables);
}
return (null, null);
}
/// <summary>
/// Gets the feeding bench information for barns and coops.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x coordinate of the tile.</param>
/// <param name="y">The y coordinate of the tile.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple of (string? name, CATEGORY? category) for the feeding bench, or null if not applicable.</returns>
private static (string? name, CATEGORY? category)? GetFeedingBenchInfo(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
string locationName = currentLocation.Name;
if (FeedingBenchBounds.TryGetValue(locationName, out var bounds) && x >= bounds.minX && x <= bounds.maxX && y == bounds.y)
{
(string? name, CATEGORY category) = TileInfo.getObjectAtTile(currentLocation, x, y, true);
return (name?.Contains("hay", StringComparison.OrdinalIgnoreCase) == true ? "Feeding Bench" : "Empty Feeding Bench", category);
}
return null;
}
/// <summary>
/// Gets information about the current location by its name.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x coordinate of the tile.</param>
/// <param name="y">The y coordinate of the tile.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple of (string? name, CATEGORY? category) for the object in the location, or null if not applicable.</returns>
private static (string? name, CATEGORY? category) GetLocationByNameInfo(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
object locationType = currentLocation is not null and GameLocation ? currentLocation.Name ?? "Undefined GameLocation" : currentLocation!.GetType(); string locationName = currentLocation.Name ?? "";
if (locationName.Contains("coop", StringComparison.OrdinalIgnoreCase) || locationName.Contains("barn", StringComparison.OrdinalIgnoreCase))
{
var feedingBenchInfo = GetFeedingBenchInfo(currentLocation, x, y);
if (feedingBenchInfo.HasValue)
{
return feedingBenchInfo.Value;
} // else if something other than feeding benches in barns and coops...
} //else if something other than barns and coops...
// Unimplemented locations are logged.
// Check if the location has already been logged
if (!loggedLocations.Contains(locationType))
{
// Log the message
MainClass.DebugLog($"Called GetLocationByNameInfo with unimplemented GameLocation of type {currentLocation.GetType()} and name {currentLocation.Name}");
// Add the location to the HashSet to prevent logging it again
loggedLocations.Add(locationType);
}
return (null, null);
}
/// <summary>
/// Retrieves the dynamic tile information for the given coordinates in the specified location.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="lessInfo">An optional boolean to return less detailed information. Defaults to false.</param>
/// <returns>A tuple containing the name and CATEGORY of the dynamic tile, or null values if not found.</returns>
public static (string? name, CATEGORY? category) GetDynamicTileAt(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
// Check for panning spots
if (currentLocation.orePanPoint.Value != Point.Zero && currentLocation.orePanPoint.Value == new Point(x, y))
{
return ("panning spot", CATEGORY.Interactables);
}
// Check if the current location has an event
else if (currentLocation.currentEvent is not null)
{
string eventName = currentLocation.currentEvent.FestivalName;
// Attempt to retrieve the nested dictionary for the event name from the EventInteractables dictionary
if (EventInteractables.TryGetValue(eventName, out var coordinateDictionary))
{
// Attempt to retrieve the interactable value from the nested dictionary using the coordinates (x, y) as the key
if (coordinateDictionary.TryGetValue((x, y), value: out var interactable))
{
// If the interactable value is found, return the corresponding category and interactable name
return (interactable, CATEGORY.Interactables);
}
}
}
// Retrieve dynamic tile information based on the current location type
return currentLocation switch
{
Beach beach => GetBeachInfo(beach, x, y, lessInfo),
BoatTunnel boatTunnel => GetBoatTunnelInfo(boatTunnel, x, y, lessInfo),
CommunityCenter communityCenter => GetCommunityCenterInfo(communityCenter, x, y, lessInfo),
Farm farm => GetFarmInfo(farm, x, y, lessInfo),
FarmHouse farmHouse => GetFarmHouseInfo(farmHouse, x, y, lessInfo),
Forest forest => GetForestInfo(forest, x, y, lessInfo),
IslandFarmHouse islandFarmHouse => GetIslandFarmHouseInfo(islandFarmHouse, x, y, lessInfo),
IslandLocation islandLocation => GetIslandLocationInfo(islandLocation, x, y, lessInfo),
LibraryMuseum libraryMuseum => GetLibraryMuseumInfo(libraryMuseum, x, y, lessInfo),
Town town => GetTownInfo(town, x, y, lessInfo),
_ => GetLocationByNameInfo(currentLocation, x, y, lessInfo)
};
}
}
}

View File

@@ -0,0 +1,218 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Features
{
internal class InventoryUtils
{
internal static string hoveredItemQueryKey = "";
internal static int prevSlotIndex = -999;
internal static bool narrateHoveredSlot(InventoryMenu inventoryMenu, List<ClickableComponent> inventory, IList<Item> actualInventory, int x, int y,
bool? giveExtraDetails = null, int hoverPrice = -1, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1,
bool handleHighlightedItem = false, String highlightedItemPrefix = "", String highlightedItemSuffix = "")
{
if (narrateHoveredSlotAndReturnIndex(inventoryMenu, inventory, actualInventory, x, y,
giveExtraDetails, hoverPrice, extraItemToShowIndex, extraItemToShowAmount,
handleHighlightedItem, highlightedItemPrefix, highlightedItemSuffix) == -999)
return false;
return true;
}
internal static int narrateHoveredSlotAndReturnIndex(InventoryMenu inventoryMenu, List<ClickableComponent> inventory, IList<Item> actualInventory, int x, int y,
bool? giveExtraDetails = null, int hoverPrice = -1, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1,
bool handleHighlightedItem = false, String highlightedItemPrefix = "", String highlightedItemSuffix = "")
{
if (giveExtraDetails is null)
giveExtraDetails = !MainClass.Config.DisableInventoryVerbosity;
for (int i = 0; i < inventory.Count; i++)
{
if (!inventory[i].containsPoint(x, y)) continue;
if ((i + 1) > actualInventory.Count || actualInventory[i] == null)
{
// For empty slot
checkAndSpeak("Empty Slot", i);
prevSlotIndex = i;
return i;
}
string toSpeak = "";
bool isHighlighted = inventoryMenu.highlightMethod(actualInventory[i]);
string namePrefix = handleHighlightedItemPrefix(isHighlighted, highlightedItemPrefix);
string nameSuffix = $"{handleHighlightedItemSuffix(isHighlighted, highlightedItemSuffix)}{handleUnHighlightedItem(isHighlighted, i)}";
string name = $"{namePrefix}{actualInventory[i].DisplayName}{nameSuffix}";
int stack = actualInventory[i].Stack;
string quality = getQualityFromItem(actualInventory[i]);
string healthNStamina = getHealthNStaminaFromItem(actualInventory[i]);
string buffs = getBuffsFromItem(actualInventory[i]);
string description = actualInventory[i].getDescription();
string price = getPrice(hoverPrice);
string requirements = getExtraItemInfo(extraItemToShowIndex, extraItemToShowAmount);
string details;
if (giveExtraDetails == true)
{
if (stack > 1)
toSpeak = $"{stack} {name}"; // {quality}, \n{requirements}, \n{price}, \n{description}, \n{healthNStamina}, \n{buffs}";
else
toSpeak = $"{name}"; //{quality}, \n{requirements}, \n{price}, \n{description}, \n{healthNStamina}, \n{buffs}";
details = string.Join(",\n", new string[] { quality, requirements, price, description, healthNStamina, buffs }.Where(c => !string.IsNullOrEmpty(c)));
}
else
{
if (stack > 1)
toSpeak = $"{stack} {name}"; //{quality}, \n{requirements}, \n{price}";
else
toSpeak = $"{name}"; //{quality}, \n{requirements}, \n{price}";
details = string.Join(",\n", new string[] { quality, requirements, price }.Where(c => !string.IsNullOrEmpty(c)));
}
if (!string.IsNullOrEmpty(details))
toSpeak = $"{toSpeak}, {details}";
checkAndSpeak(toSpeak, i);
prevSlotIndex = i;
return i;
}
// If no slot is hovered
return -999;
}
private static void checkAndSpeak(String toSpeak, int hoveredInventoryIndex)
{
if (hoveredItemQueryKey == $"{toSpeak}:{hoveredInventoryIndex}") return;
hoveredItemQueryKey = $"{toSpeak}:{hoveredInventoryIndex}";
MainClass.ScreenReader.Say(toSpeak, true);
}
private static String getQualityFromItem(Item item)
{
if (item is not StardewValley.Object || ((StardewValley.Object)item).Quality <= 0)
return "";
int qualityIndex = ((StardewValley.Object)item).Quality;
if (qualityIndex == 1)
{
return "Silver quality";
}
else if (qualityIndex == 2 || qualityIndex == 3)
{
return "Gold quality";
}
else if (qualityIndex >= 4)
{
return "Iridium quality";
}
return "";
}
private static String getHealthNStaminaFromItem(Item item)
{
if (item is not StardewValley.Object || ((StardewValley.Object)item).Edibility == -300)
return "";
String toReturn = "";
int stamina_recovery = ((StardewValley.Object)item).staminaRecoveredOnConsumption();
toReturn += $"{stamina_recovery} Energy";
if (stamina_recovery < 0) return toReturn;
int health_recovery = ((StardewValley.Object)item).healthRecoveredOnConsumption();
toReturn += $"\n\t{health_recovery} Health";
return toReturn;
}
private static String getBuffsFromItem(Item item)
{
if (item == null) return "";
if (item is not StardewValley.Object) return "";
if (((StardewValley.Object)item) == null) return "";
// These variables are taken from the game's code itself (IClickableMenu.cs -> 1016 line)
bool edibleItem = (int)((StardewValley.Object)item).Edibility != -300;
string[]? buffIconsToDisplay = (edibleItem && Game1.objectInformation[((StardewValley.Object)item).ParentSheetIndex].Split('/').Length > 7)
? item.ModifyItemBuffs(Game1.objectInformation[((StardewValley.Object)item).ParentSheetIndex].Split('/')[7].Split(' '))
: null;
if (buffIconsToDisplay == null)
return "";
String toReturn = "";
for (int j = 0; j < buffIconsToDisplay.Length; j++)
{
string buffName = ((Convert.ToInt32(buffIconsToDisplay[j]) > 0) ? "+" : "") + buffIconsToDisplay[j] + " ";
if (j <= 11)
{
buffName = Game1.content.LoadString("Strings\\UI:ItemHover_Buff" + j, buffName);
}
try
{
int count = int.Parse(buffName.Substring(0, buffName.IndexOf(' ')));
if (count != 0)
toReturn += $"{buffName}\n";
}
catch (Exception) { }
}
return toReturn;
}
private static String getExtraItemInfo(int itemIndex, int itemAmount)
{
if (itemIndex == -1) return "";
string itemName = Game1.objectInformation[itemIndex].Split('/')[0];
if (itemAmount != -1)
return $"Required: {itemAmount} {itemName}";
else
return $"Required: {itemName}";
}
private static String getPrice(int price)
{
if (price == -1) return "";
return $"Sell Price: {price} g";
}
private static String handleHighlightedItemPrefix(bool isHighlighted, String prefix)
{
if (MainClass.Config.DisableInventoryVerbosity) return "";
if (!isHighlighted) return "";
return prefix;
}
private static String handleHighlightedItemSuffix(bool isHighlighted, String suffix)
{
if (MainClass.Config.DisableInventoryVerbosity) return "";
if (!isHighlighted) return "";
return suffix;
}
private static String handleUnHighlightedItem(bool isHighlighted, int hoveredInventoryIndex)
{
if (isHighlighted) return "";
if (prevSlotIndex != hoveredInventoryIndex)
Game1.playSound("invalid-selection");
if (MainClass.Config.DisableInventoryVerbosity) return "";
return " not usable here";
}
internal static void Cleanup()
{
hoveredItemQueryKey = "";
prevSlotIndex = -999;
}
}
}

View File

@@ -23,7 +23,7 @@ namespace stardew_access.Features
return; return;
previousSlotItem = currentSlotItem; previousSlotItem = currentSlotItem;
MainClass.GetScreenReader().Say($"{currentSlotItem.DisplayName} Selected", true); MainClass.ScreenReader.Say($"{currentSlotItem.DisplayName} Selected", true);
} }
// Narrates current location's name // Narrates current location's name
@@ -38,38 +38,11 @@ namespace stardew_access.Features
return; return;
previousLocation = currentLocation; previousLocation = currentLocation;
MainClass.GetScreenReader().Say($"{currentLocation.Name} Entered", true); MainClass.ScreenReader.Say($"{currentLocation.Name} Entered", true);
} }
public static void SnapMouseToPlayer() public static void narrateHudMessages()
{ {
int x = Game1.player.GetBoundingBox().Center.X - Game1.viewport.X;
int y = Game1.player.GetBoundingBox().Center.Y - Game1.viewport.Y;
int offset = 64;
switch (Game1.player.FacingDirection)
{
case 0:
y -= offset;
break;
case 1:
x += offset;
break;
case 2:
y += offset;
break;
case 3:
x -= offset;
break;
}
Game1.setMousePosition(x, y);
}
public static async void narrateHudMessages()
{
MainClass.isNarratingHudMessage = true;
try try
{ {
if (Game1.hudMessages.Count > 0) if (Game1.hudMessages.Count > 0)
@@ -89,18 +62,16 @@ namespace stardew_access.Features
{ {
MainClass.hudMessageQueryKey = searchQuery; MainClass.hudMessageQueryKey = searchQuery;
MainClass.GetScreenReader().Say(toSpeak, true); MainClass.ScreenReader.Say(toSpeak, true);
} }
} }
} }
} }
catch (Exception e) catch (Exception e)
{ {
MainClass.GetMonitor().Log($"Unable to narrate hud messages:\n{e.Message}\n{e.StackTrace}", StardewModdingAPI.LogLevel.Error); MainClass.ErrorLog($"Unable to narrate hud messages:\n{e.Message}\n{e.StackTrace}");
} }
await Task.Delay(300);
MainClass.isNarratingHudMessage = false;
} }
} }
} }

View File

@@ -1,53 +1,14 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using StardewValley; using StardewValley;
using StardewValley.Objects; using StardewValley.Objects;
using StardewValley.TerrainFeatures;
namespace stardew_access.Features namespace stardew_access.Features
{ {
/// <summary>
/// This is a custom enum class and contains the name of groups the objects are divided into for the feature
/// </summary>
public class CATEGORY
{
private string _typeKeyWord;
private CATEGORY(string typeKeyWord)
{
_typeKeyWord = typeKeyWord;
}
public override string ToString()
{
return _typeKeyWord;
}
public static CATEGORY Farmers = new CATEGORY("farmer");
public static CATEGORY FarmAnimals = new CATEGORY("animal");
public static CATEGORY NPCs = new CATEGORY("npc");
public static CATEGORY Furnitures = new CATEGORY("furniture");
public static CATEGORY Flooring = new CATEGORY("flooring");
public static CATEGORY Debris = new CATEGORY("debris");
public static CATEGORY Crops = new CATEGORY("crop");
public static CATEGORY Trees = new CATEGORY("tree");
public static CATEGORY Bush = new CATEGORY("bush");
public static CATEGORY Buildings = new CATEGORY("building");
public static CATEGORY MineItems = new CATEGORY("mine item");
public static CATEGORY ResourceClumps = new CATEGORY("resource clump");
public static CATEGORY Chests = new CATEGORY("chest");
public static CATEGORY JunimoBundle = new CATEGORY("bundle");
public static CATEGORY Doors = new CATEGORY("door"); // Also includes ladders and elevators
public static CATEGORY Others = new CATEGORY("other");
public static CATEGORY WaterTiles = new CATEGORY("water");
}
public class Radar public class Radar
{ {
private List<Vector2> closed; private readonly List<Vector2> closed;
private List<Furniture> furnitures; private readonly List<Furniture> furnitures;
private List<NPC> npcs; private readonly List<NPC> npcs;
public List<string> exclusions; public List<string> exclusions;
private List<string> temp_exclusions; private List<string> temp_exclusions;
public List<string> focus; public List<string> focus;
@@ -80,7 +41,11 @@ namespace stardew_access.Features
exclusions.Add("tree"); exclusions.Add("tree");
exclusions.Add("flooring"); exclusions.Add("flooring");
exclusions.Add("water"); exclusions.Add("water");
exclusions.Add("debris");
exclusions.Add("grass"); exclusions.Add("grass");
exclusions.Add("decoration");
exclusions.Add("bridge");
exclusions.Add("other");
/* Not excluded Categories /* Not excluded Categories
* *
@@ -89,19 +54,22 @@ namespace stardew_access.Features
* exclusions.Add("animal"); * exclusions.Add("animal");
* exclusions.Add("npc"); * exclusions.Add("npc");
* exclusions.Add("furniture") * exclusions.Add("furniture")
* exclusions.Add("other");
* exclusions.Add("building"); * exclusions.Add("building");
* exclusions.Add("resource clump");
* exclusions.Add("mine item"); * exclusions.Add("mine item");
* exclusions.Add("chest"); * exclusions.Add("container");
* exclusions.Add("bundle");
* exclusions.Add("door");
* exclusions.Add("machine");
* exclusions.Add("interactable");
*/ */
} }
public async void Run() public void Run()
{ {
if (MainClass.radarDebug) if (MainClass.radarDebug)
MainClass.GetMonitor().Log($"\n\nRead Tile started", StardewModdingAPI.LogLevel.Debug); MainClass.DebugLog($"\n\nRead Tile started");
isRunning = true;
Vector2 currPosition = Game1.player.getTileLocation(); Vector2 currPosition = Game1.player.getTileLocation();
closed.Clear(); closed.Clear();
@@ -111,10 +79,7 @@ namespace stardew_access.Features
SearchNearbyTiles(currPosition, range); SearchNearbyTiles(currPosition, range);
if (MainClass.radarDebug) if (MainClass.radarDebug)
MainClass.GetMonitor().Log($"\nRead Tile stopped\n\n", StardewModdingAPI.LogLevel.Debug); MainClass.DebugLog($"\nRead Tile stopped\n\n");
await Task.Delay(delay);
isRunning = false;
} }
/// <summary> /// <summary>
@@ -126,13 +91,13 @@ namespace stardew_access.Features
/// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns> /// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns>
public Dictionary<Vector2, (string, string)> SearchNearbyTiles(Vector2 center, int limit, bool playSound = true) public Dictionary<Vector2, (string, string)> SearchNearbyTiles(Vector2 center, int limit, bool playSound = true)
{ {
Dictionary<Vector2, (string, string)> detectedTiles = new Dictionary<Vector2, (string, string)>(); var currentLocation = Game1.currentLocation;
Dictionary<Vector2, (string, string)> detectedTiles = new();
Queue<Vector2> toSearch = new Queue<Vector2>(); Queue<Vector2> toSearch = new();
List<Vector2> searched = new List<Vector2>(); HashSet<Vector2> searched = new();
int[] dirX = { -1, 0, 1, 0 }; int[] dirX = { -1, 0, 1, 0 };
int[] dirY = { 0, 1, 0, -1 }; int[] dirY = { 0, 1, 0, -1 };
int count = 0;
toSearch.Enqueue(center); toSearch.Enqueue(center);
searched.Add(center); searched.Add(center);
@@ -141,21 +106,20 @@ namespace stardew_access.Features
{ {
Vector2 item = toSearch.Dequeue(); Vector2 item = toSearch.Dequeue();
if (playSound) if (playSound)
CheckTileAndPlaySound(item); CheckTileAndPlaySound(item, currentLocation);
else else
{ {
(bool, string?, string) tileInfo = CheckTile(item); (bool, string?, string) tileInfo = CheckTile(item, currentLocation);
if (tileInfo.Item1 && tileInfo.Item2 != null) if (tileInfo.Item1 && tileInfo.Item2 != null)
{ {
// Add detected tile to the dictionary // Add detected tile to the dictionary
detectedTiles.Add(item, (tileInfo.Item2, tileInfo.Item3)); detectedTiles.Add(item, (tileInfo.Item2, tileInfo.Item3));
} }
} }
count++;
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
Vector2 dir = new Vector2(item.X + dirX[i], item.Y + dirY[i]); Vector2 dir = new(item.X + dirX[i], item.Y + dirY[i]);
if (isValid(dir, center, searched, limit)) if (isValid(dir, center, searched, limit))
{ {
@@ -165,6 +129,64 @@ namespace stardew_access.Features
} }
} }
searched.Clear();
return detectedTiles;
}
/// <summary>
/// Search the entire location using Breadth First Search algorithm(BFS).
/// </summary>
/// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns>
public Dictionary<Vector2, (string, string)> SearchLocation()
{
//var watch = new Stopwatch();
//watch.Start();
var currentLocation = Game1.currentLocation;
Dictionary<Vector2, (string, string)> detectedTiles = new();
Vector2 position = Vector2.Zero;
(bool, string? name, string category) tileInfo;
Queue<Vector2> toSearch = new();
HashSet<Vector2> searched = new();
int[] dirX = { -1, 0, 1, 0 };
int[] dirY = { 0, 1, 0, -1 };
int count = 0;
toSearch.Enqueue(Game1.player.getTileLocation());
searched.Add(Game1.player.getTileLocation());
//watch.Stop();
//var elapsedMs = watch.ElapsedMilliseconds;
//MainClass.DebugLog($"Search init duration: {elapsedMs}");
//watch.Reset();
//watch.Start();
while (toSearch.Count > 0)
{
Vector2 item = toSearch.Dequeue();
tileInfo = CheckTile(item, currentLocation, true);
if (tileInfo.Item1 && tileInfo.name != null)
{
// Add detected tile to the dictionary
detectedTiles.Add(item, (tileInfo.name, tileInfo.category));
}
count++;
for (int i = 0; i < 4; i++)
{
Vector2 dir = new(item.X + dirX[i], item.Y + dirY[i]);
if (!searched.Contains(dir) && (TileInfo.isWarpPointAtTile(currentLocation, (int)dir.X, (int)dir.Y) || currentLocation.isTileOnMap(dir)))
{
toSearch.Enqueue(dir);
searched.Add(dir);
}
}
}
//watch.Stop();
//elapsedMs = watch.ElapsedMilliseconds;
//MainClass.DebugLog($"Search loop duration: {elapsedMs}; {count} iterations.");
searched.Clear();
return detectedTiles; return detectedTiles;
} }
@@ -176,7 +198,7 @@ namespace stardew_access.Features
/// <param name="searched">The list of searched items.</param> /// <param name="searched">The list of searched items.</param>
/// <param name="limit">The radius of search</param> /// <param name="limit">The radius of search</param>
/// <returns>Returns true if the tile is valid for search.</returns> /// <returns>Returns true if the tile is valid for search.</returns>
public bool isValid(Vector2 item, Vector2 center, List<Vector2> searched, int limit) public bool isValid(Vector2 item, Vector2 center, HashSet<Vector2> searched, int limit)
{ {
if (Math.Abs(item.X - center.X) > limit) if (Math.Abs(item.X - center.X) > limit)
return false; return false;
@@ -189,29 +211,28 @@ namespace stardew_access.Features
return true; return true;
} }
public (bool, string?, string) CheckTile(Vector2 position) public (bool, string? name, string category) CheckTile(Vector2 position, GameLocation currentLocation, bool lessInfo = false)
{ {
(string?, CATEGORY?) tileDetail = ReadTile.getNameWithCategoryAtTile(position); (string? name, CATEGORY? category) = TileInfo.getNameWithCategoryAtTile(position, currentLocation, lessInfo);
if (tileDetail.Item1 == null) if (name == null)
return (false, null, CATEGORY.Others.ToString()); return (false, null, CATEGORY.Others.ToString());
if (tileDetail.Item2 == null) category ??= CATEGORY.Others;
tileDetail.Item2 = CATEGORY.Others;
return (true, tileDetail.Item1, tileDetail.Item2.ToString());
return (true, name, category.ToString());
} }
public void CheckTileAndPlaySound(Vector2 position) public void CheckTileAndPlaySound(Vector2 position, GameLocation currentLocation)
{ {
try try
{ {
if (Game1.currentLocation.isObjectAtTile((int)position.X, (int)position.Y)) if (currentLocation.isObjectAtTile((int)position.X, (int)position.Y))
{ {
(string?, CATEGORY) objDetails = ReadTile.getObjectAtTile((int)position.X, (int)position.Y); (string? name, CATEGORY category) objDetails = TileInfo.getObjectAtTile(currentLocation, (int)position.X, (int)position.Y);
string? objectName = objDetails.Item1; string? objectName = objDetails.name;
CATEGORY category = objDetails.Item2; CATEGORY category = objDetails.category;
StardewValley.Object obj = Game1.currentLocation.getObjectAtTile((int)position.X, (int)position.Y); StardewValley.Object obj = currentLocation.getObjectAtTile((int)position.X, (int)position.Y);
if (objectName != null) if (objectName != null)
{ {
@@ -222,38 +243,37 @@ namespace stardew_access.Features
if (!furnitures.Contains((Furniture)obj)) if (!furnitures.Contains((Furniture)obj))
{ {
furnitures.Add((Furniture)obj); furnitures.Add((Furniture)obj);
PlaySoundAt(position, objectName, category); PlaySoundAt(position, objectName, category, currentLocation);
} }
} }
else else
PlaySoundAt(position, objectName, category); PlaySoundAt(position, objectName, category, currentLocation);
} }
} }
else else
{ {
(string?, CATEGORY?) tileDetail = ReadTile.getNameWithCategoryAtTile(position); (string? name, CATEGORY? category) = TileInfo.getNameWithCategoryAtTile(position, currentLocation);
if (tileDetail.Item1 != null) if (name != null)
{ {
if (tileDetail.Item2 == null) category ??= CATEGORY.Others;
tileDetail.Item2 = CATEGORY.Others;
PlaySoundAt(position, tileDetail.Item1, tileDetail.Item2); PlaySoundAt(position, name, category, currentLocation);
} }
} }
} }
catch (Exception e) catch (Exception e)
{ {
MainClass.GetMonitor().Log($"{e.Message}\n{e.StackTrace}\n{e.Source}", StardewModdingAPI.LogLevel.Error); MainClass.ErrorLog($"{e.Message}\n{e.StackTrace}\n{e.Source}");
} }
} }
public void PlaySoundAt(Vector2 position, string searchQuery, CATEGORY category) public void PlaySoundAt(Vector2 position, string searchQuery, CATEGORY category, GameLocation currentLocation)
{ {
#region Check whether to skip the object or not #region Check whether to skip the object or not
// Skip if player is directly looking at the tile // Skip if player is directly looking at the tile
if (CurrentPlayer.getNextTile().Equals(position)) if (CurrentPlayer.FacingTile.Equals(position))
return; return;
if (!radarFocus) if (!radarFocus)
@@ -296,7 +316,7 @@ namespace stardew_access.Features
#endregion #endregion
if (MainClass.radarDebug) if (MainClass.radarDebug)
MainClass.GetMonitor().Log($"{radarFocus}\tObject:{searchQuery.ToLower().Trim()}\tPosition: X={position.X} Y={position.Y}", StardewModdingAPI.LogLevel.Debug); MainClass.ErrorLog($"{radarFocus}\tObject:{searchQuery.ToLower().Trim()}\tPosition: X={position.X} Y={position.Y}");
int px = (int)Game1.player.getTileX(); // Player's X postion int px = (int)Game1.player.getTileX(); // Player's X postion
int py = (int)Game1.player.getTileY(); // Player's Y postion int py = (int)Game1.player.getTileY(); // Player's Y postion
@@ -309,19 +329,19 @@ namespace stardew_access.Features
if (dy < 0 && (Math.Abs(dy) >= Math.Abs(dx))) // Object is at top if (dy < 0 && (Math.Abs(dy) >= Math.Abs(dx))) // Object is at top
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "top"), position); currentLocation.localSoundAt(GetSoundName(category, "top"), position);
} }
else if (dx > 0 && (Math.Abs(dx) >= Math.Abs(dy))) // Object is at right else if (dx > 0 && (Math.Abs(dx) >= Math.Abs(dy))) // Object is at right
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "right"), position); currentLocation.localSoundAt(GetSoundName(category, "right"), position);
} }
else if (dx < 0 && (Math.Abs(dx) > Math.Abs(dy))) // Object is at left else if (dx < 0 && (Math.Abs(dx) > Math.Abs(dy))) // Object is at left
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "left"), position); currentLocation.localSoundAt(GetSoundName(category, "left"), position);
} }
else if (dy > 0 && (Math.Abs(dy) > Math.Abs(dx))) // Object is at bottom else if (dy > 0 && (Math.Abs(dy) > Math.Abs(dx))) // Object is at bottom
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "bottom"), position); currentLocation.localSoundAt(GetSoundName(category, "bottom"), position);
} }
} }
@@ -330,7 +350,7 @@ namespace stardew_access.Features
{ {
string soundName = $"_{post}"; string soundName = $"_{post}";
if (!MainClass.radarStereoSound) if (!MainClass.Config.RadarStereoSound)
soundName = $"_mono{soundName}"; soundName = $"_mono{soundName}";
if (category == CATEGORY.Farmers) // Villagers and farmers if (category == CATEGORY.Farmers) // Villagers and farmers
@@ -353,7 +373,7 @@ namespace stardew_access.Features
soundName = $"obj{soundName}"; soundName = $"obj{soundName}";
else if (category == CATEGORY.MineItems) // Mine items else if (category == CATEGORY.MineItems) // Mine items
soundName = $"obj{soundName}"; soundName = $"obj{soundName}";
else if (category == CATEGORY.Chests) // Chests else if (category == CATEGORY.Containers) // Chests
soundName = $"obj{soundName}"; soundName = $"obj{soundName}";
else if (category == CATEGORY.Debris) // Grass and debris else if (category == CATEGORY.Debris) // Grass and debris
soundName = $"obj{soundName}"; soundName = $"obj{soundName}";

View File

@@ -1,54 +1,106 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using StardewModdingAPI; using StardewModdingAPI;
using StardewValley; using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.Objects;
using StardewValley.TerrainFeatures;
namespace stardew_access.Features namespace stardew_access.Features
{ {
/// <summary>
/// Reads the name and information about a tile.
/// </summary>
public class ReadTile public class ReadTile
{ {
public static bool isReadingTile = false; private bool isBusy; // To pause execution of run method between fixed intervals
public static Vector2 prevTile; private int delay; // Length of each interval (in ms)
private bool shouldPause; // To pause the execution
private Vector2 prevTile;
public ReadTile() public ReadTile()
{ {
isReadingTile = false; isBusy = false;
delay = 100;
} }
public static async void run(bool manuallyTriggered = false) public void update()
{ {
isReadingTile = true; if (this.isBusy)
return;
if (this.shouldPause)
return;
this.isBusy = true;
this.run();
Task.Delay(delay).ContinueWith(_ => { this.isBusy = false; });
}
/// <summary>
/// Pauses the feature for the provided time.
/// </summary>
/// <param name="time">The amount of time we want to pause the execution (in ms).<br/>Default is 2500 (2.5s).</param>
public void pauseUntil(int time = 2500)
{
this.shouldPause = true;
Task.Delay(time).ContinueWith(_ => { this.shouldPause = false; });
}
/// <summary>
/// Pauses the feature
/// </summary>
public void pause()
{
this.shouldPause = true;
}
/// <summary>
/// Resumes the feature
/// </summary>
public void resume()
{
this.shouldPause = false;
}
public void run(bool manuallyTriggered = false, bool playersPosition = false)
{
try try
{ {
#region Get Next Grab Tile Vector2 tile;
Vector2 tile = CurrentPlayer.getNextTile();
int x = (int)tile.X; #region Get Tile
int y = (int)tile.Y; int x, y;
if (!playersPosition)
{
// Grab tile
tile = CurrentPlayer.FacingTile;
}
else
{
// Player's standing tile
tile = CurrentPlayer.Position;
}
x = (int)tile.X;
y = (int)tile.Y;
#endregion #endregion
if (Context.IsPlayerFree) if (Context.IsPlayerFree)
{ {
if (!manuallyTriggered && prevTile != tile) if (!manuallyTriggered && prevTile != tile)
{ {
if (MainClass.GetScreenReader() != null) if (MainClass.ScreenReader != null)
MainClass.GetScreenReader().PrevTextTile = " "; MainClass.ScreenReader.PrevTextTile = " ";
} }
bool isColliding = isCollidingAtTile(x, y); var currentLocation = Game1.currentLocation;
bool isColliding = TileInfo.IsCollidingAtTile(currentLocation, x, y);
string? toSpeak = getNameAtTile(tile); (string? name, string? category) info = TileInfo.getNameWithCategoryNameAtTile(tile, currentLocation);
#region Narrate toSpeak #region Narrate toSpeak
if (toSpeak != null) if (info.name != null)
if (MainClass.GetScreenReader() != null) if (MainClass.ScreenReader != null)
if (manuallyTriggered) if (manuallyTriggered)
MainClass.GetScreenReader().Say(toSpeak, true); MainClass.ScreenReader.Say($"{info.name}, Category: {info.category}", true);
else else
MainClass.GetScreenReader().SayWithTileQuery(toSpeak, x, y, true); MainClass.ScreenReader.SayWithTileQuery(info.name, x, y, true);
#endregion #endregion
#region Play colliding sound effect #region Play colliding sound effect
@@ -58,865 +110,15 @@ namespace stardew_access.Features
} }
#endregion #endregion
prevTile = tile; if (!manuallyTriggered)
prevTile = tile;
} }
} }
catch (Exception e) catch (Exception e)
{ {
MainClass.GetMonitor().Log($"Error in Read Tile:\n{e.Message}\n{e.StackTrace}", LogLevel.Debug); MainClass.ErrorLog($"Error in Read Tile:\n{e.Message}\n{e.StackTrace}");
} }
await Task.Delay(100);
isReadingTile = false;
}
///<summary>Returns the name of the object at tile alongwith it's category's name</summary>
public static (string?, string?) getNameWithCategoryNameAtTile(Vector2 tile)
{
(string?, CATEGORY?) tileDetail = getNameWithCategoryAtTile(tile);
if (tileDetail.Item2 == null)
tileDetail.Item2 = CATEGORY.Others;
return (tileDetail.Item1, tileDetail.Item2.ToString());
}
///<summary>Returns the name of the object at tile alongwith it's category</summary>
public static (string?, CATEGORY?) getNameWithCategoryAtTile(Vector2 tile)
{
int x = (int)tile.X;
int y = (int)tile.Y;
string? toReturn = "";
CATEGORY? category = CATEGORY.Others;
bool isColliding = isCollidingAtTile(x, y);
Dictionary<Vector2, Netcode.NetRef<TerrainFeature>> terrainFeature = Game1.currentLocation.terrainFeatures.FieldDict;
string? door = getDoorAtTile(x, y);
(CATEGORY?, string?) tileInfo = getTileInfo(x, y);
string? junimoBundle = getJunimoBundleAt(x, y);
string? resourceClump = getResourceClumpAtTile(x, y);
string? farmAnimal = getFarmAnimalAt(Game1.currentLocation, x, y);
if (Game1.currentLocation.isCharacterAtTile(tile) != null)
{
NPC npc = Game1.currentLocation.isCharacterAtTile(tile);
toReturn = npc.displayName;
if (npc.isVillager() || npc.CanSocialize)
category = CATEGORY.Farmers;
else
category = CATEGORY.NPCs;
}
else if (farmAnimal != null)
{
toReturn = farmAnimal;
category = CATEGORY.FarmAnimals;
}
else if (Game1.currentLocation.isWaterTile(x, y) && isColliding)
{
toReturn = "Water";
category = CATEGORY.WaterTiles;
}
else if (Game1.currentLocation.isObjectAtTile(x, y))
{
(string?, CATEGORY?) obj = getObjectAtTile(x, y);
toReturn = obj.Item1;
category = obj.Item2;
}
else if (terrainFeature.ContainsKey(tile))
{
(string?, CATEGORY) tf = getTerrainFeatureAtTile(terrainFeature[tile]);
string? terrain = tf.Item1;
if (terrain != null)
{
toReturn = terrain;
category = tf.Item2;
}
}
else if (Game1.currentLocation.getLargeTerrainFeatureAt(x, y) != null)
{
toReturn = getBushAtTile(x, y);
category = CATEGORY.Bush;
}
else if (resourceClump != null)
{
toReturn = resourceClump;
category = CATEGORY.ResourceClumps;
}
else if (door != null)
{
toReturn = door;
category = CATEGORY.Doors;
}
else if (isMineDownLadderAtTile(x, y))
{
toReturn = "Ladder";
category = CATEGORY.Doors;
}
else if (isMineUpLadderAtTile(x, y))
{
toReturn = "Up Ladder";
category = CATEGORY.Doors;
}
else if (isElevatorAtTile(x, y))
{
toReturn = "Elevator";
category = CATEGORY.Doors;
}
else if (tileInfo.Item2 != null)
{
toReturn = tileInfo.Item2;
category = tileInfo.Item1;
}
else if (junimoBundle != null)
{
toReturn = junimoBundle;
category = CATEGORY.JunimoBundle;
}
if (toReturn == "")
return (null, category);
return (toReturn, category);
}
///<summary>Returns the name of the object at tile</summary>
public static string? getNameAtTile(Vector2 tile)
{
int x = (int)tile.X;
int y = (int)tile.Y;
string? toReturn = "";
bool isColliding = isCollidingAtTile(x, y);
Dictionary<Vector2, Netcode.NetRef<TerrainFeature>> terrainFeature = Game1.currentLocation.terrainFeatures.FieldDict;
string? door = getDoorAtTile(x, y);
(CATEGORY?, string?) tileInfo = getTileInfo(x, y);
string? junimoBundle = getJunimoBundleAt(x, y);
string? resourceClump = getResourceClumpAtTile(x, y);
string? farmAnimal = getFarmAnimalAt(Game1.currentLocation, x, y);
if (Game1.currentLocation.isCharacterAtTile(tile) != null)
{
NPC npc = Game1.currentLocation.isCharacterAtTile(tile);
toReturn = npc.displayName;
}
else if (farmAnimal != null)
{
toReturn = farmAnimal;
}
else if (Game1.currentLocation.isWaterTile(x, y) && isColliding)
{
toReturn = "Water";
}
else if (Game1.currentLocation.isObjectAtTile(x, y))
{
toReturn = getObjectAtTile(x, y).Item1;
}
else if (terrainFeature.ContainsKey(tile))
{
string? terrain = getTerrainFeatureAtTile(terrainFeature[tile]).Item1;
if (terrain != null)
toReturn = terrain;
}
else if (Game1.currentLocation.getLargeTerrainFeatureAt(x, y) != null)
{
toReturn = getBushAtTile(x, y);
}
else if (resourceClump != null)
{
toReturn = resourceClump;
}
else if (door != null)
{
toReturn = door;
}
else if (isMineDownLadderAtTile(x, y))
{
toReturn = "Ladder";
}
else if (isMineUpLadderAtTile(x, y))
{
toReturn = "Up Ladder";
}
else if (isElevatorAtTile(x, y))
{
toReturn = "Elevator";
}
else if (tileInfo.Item2 != null)
{
toReturn = tileInfo.Item2;
}
else if (junimoBundle != null)
{
toReturn = junimoBundle;
}
if (toReturn == "")
return null;
return toReturn;
}
public static string? getBushAtTile(int x, int y)
{
string? toReturn = null;
Bush bush = (Bush)Game1.currentLocation.getLargeTerrainFeatureAt(x, y);
int size = bush.size;
#region Check if bush is harvestable or not
if (!bush.townBush && (int)bush.tileSheetOffset == 1 && bush.inBloom(Game1.GetSeasonForLocation(Game1.currentLocation), Game1.dayOfMonth))
{
// Taken from the game's code
string season = ((int)bush.overrideSeason == -1) ? Game1.GetSeasonForLocation(Game1.currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason);
int shakeOff = -1;
if (!(season == "spring"))
{
if (season == "fall")
{
shakeOff = 410;
}
}
else
{
shakeOff = 296;
}
if ((int)size == 3)
{
shakeOff = 815;
}
if ((int)size == 4)
{
shakeOff = 73;
}
if (shakeOff == -1)
{
return null;
}
toReturn = "Harvestable";
}
#endregion
if (bush.townBush)
toReturn = $"{toReturn} Town Bush";
else if (bush.greenhouseBush)
toReturn = $"{toReturn} Greenhouse Bush";
else
toReturn = $"{toReturn} Bush";
return toReturn;
}
public static string? getJunimoBundleAt(int x, int y)
{
if (Game1.currentLocation is not CommunityCenter)
return null;
CommunityCenter communityCenter = ((CommunityCenter)Game1.currentLocation);
string? name = (x, y) switch
{
(14, 5) => "Pantry",
(14, 23) => "Crafts Room",
(40, 10) => "Fish Tank",
(63, 14) => "Boiler Room",
(55, 6) => "Vault",
(46, 11) => "Bulletin Board",
_ => null,
};
if (name != null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name)))
return $"{name} bundle";
else
return null;
}
public static bool isCollidingAtTile(int x, int y)
{
Rectangle rect = new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62);
if (Game1.currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true))
{
return true;
}
if (Game1.currentLocation is Woods && getStumpsInWoods(x, y) != null)
return true;
return false;
}
public static string? getFarmAnimalAt(GameLocation? location, int x, int y, bool onlyName = false)
{
if (location == null)
return null;
if (location is not Farm && location is not AnimalHouse)
return null;
List<FarmAnimal>? farmAnimals = null;
if (location is Farm)
farmAnimals = ((Farm)location).getAllFarmAnimals();
else if (location is AnimalHouse)
farmAnimals = ((AnimalHouse)location).animals.Values.ToList();
if (farmAnimals == null || farmAnimals.Count <= 0)
return null;
for (int i = 0; i < farmAnimals.Count; i++)
{
int fx = farmAnimals[i].getTileX();
int fy = farmAnimals[i].getTileY();
if (fx.Equals(x) && fy.Equals(y))
{
string name = farmAnimals[i].displayName;
int age = farmAnimals[i].age.Value;
string type = farmAnimals[i].displayType;
if (onlyName)
return name;
return $"{name}, {type}, age {age}";
}
}
return null;
}
/// <summary>
///
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns>Item1: This is the category of the tile. Default to Furnitures.
/// <br/>Item2: This is the name of the tile. Default to null if the tile tile has nothing on it.</returns>
public static (CATEGORY?, string?) getTileInfo(int x, int y)
{
int? index = null;
if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null)
index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex;
/* Add More
MainClass.monitor.Log(index.ToString(), LogLevel.Debug);
*/
if (Game1.currentLocation is Farm)
{
Building building = ((Farm)Game1.currentLocation).getBuildingAt(new Vector2(x, y));
if (building != null)
{
return (CATEGORY.Buildings, building.buildingType.Value);
}
}
if (index != null)
{
switch (index)
{
case 1955:
case 41:
return (CATEGORY.Furnitures, "Mail Box");
case 1003:
return (CATEGORY.Furnitures, "Street lamp");
case 78:
return (CATEGORY.Furnitures, "Trash bin");
case 617:
return (CATEGORY.Furnitures, "Daily quest");
case 616:
return (CATEGORY.Furnitures, "Calender");
}
if (Game1.currentLocation is FarmHouse || Game1.currentLocation is IslandFarmHouse)
{
switch (index)
{
case 173:
return (CATEGORY.Furnitures, "Fridge");
case 169:
case 170:
case 171:
case 172:
return (CATEGORY.Furnitures, "Kitchen");
}
}
}
return (null, null);
}
public static (string?, CATEGORY) getTerrainFeatureAtTile(Netcode.NetRef<TerrainFeature> terrain)
{
string? toReturn = null;
CATEGORY category = CATEGORY.Others;
if (terrain.Get() is HoeDirt)
{
category = CATEGORY.Crops;
HoeDirt dirt = (HoeDirt)terrain.Get();
if (dirt.crop != null)
{
string cropName = Game1.objectInformation[dirt.crop.indexOfHarvest].Split('/')[0];
toReturn = $"{cropName}";
bool isWatered = dirt.state.Value == HoeDirt.watered;
bool isHarvestable = dirt.readyForHarvest();
bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer;
if (isWatered)
toReturn = "Watered " + toReturn;
if (isFertilized)
toReturn = "Fertilized " + toReturn;
if (isHarvestable)
toReturn = "Harvestable " + toReturn;
}
else
{
toReturn = "Soil";
bool isWatered = dirt.state.Value == HoeDirt.watered;
bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer;
if (isWatered)
toReturn = "Watered " + toReturn;
if (isFertilized)
toReturn = "Fertilized " + toReturn;
}
}
else if (terrain.Get() is GiantCrop)
{
category = CATEGORY.Crops;
int whichCrop = ((GiantCrop)terrain.Get()).which.Value;
switch (whichCrop)
{
case 0:
toReturn = "Cauliflower";
break;
case 1:
toReturn = "Melon";
break;
case 2:
toReturn = "Pumpkin";
break;
}
}
else if (terrain.Get() is CosmeticPlant)
{
category = CATEGORY.Furnitures;
CosmeticPlant cosmeticPlant = (CosmeticPlant)terrain.Get();
toReturn = cosmeticPlant.textureName().ToLower();
if (toReturn.Contains("terrain"))
toReturn.Replace("terrain", "");
if (toReturn.Contains("feature"))
toReturn.Replace("feature", "");
}
else if (terrain.Get() is Flooring)
{
category = CATEGORY.Flooring;
Flooring flooring = (Flooring)terrain.Get();
bool isPathway = flooring.isPathway.Get();
bool isSteppingStone = flooring.isSteppingStone.Get();
toReturn = "Flooring";
if (isPathway)
toReturn = "Pathway";
if (isSteppingStone)
toReturn = "Stepping Stone";
}
else if (terrain.Get() is FruitTree)
{
category = CATEGORY.Trees;
toReturn = getFruitTree((FruitTree)terrain.Get());
}
else if (terrain.Get() is Grass)
{
category = CATEGORY.Debris;
toReturn = "Grass";
}
else if (terrain.Get() is Tree)
{
category = CATEGORY.Trees;
toReturn = getTree((Tree)terrain.Get());
}
else if (terrain.Get() is Quartz)
{
category = CATEGORY.MineItems;
toReturn = "Quartz";
}
return (toReturn, category);
}
public static string getFruitTree(FruitTree fruitTree)
{
int stage = fruitTree.growthStage.Value;
int fruitIndex = fruitTree.indexOfFruit.Get();
string toReturn = Game1.objectInformation[fruitIndex].Split('/')[0];
if (stage == 0)
toReturn = $"{toReturn} seed";
else if (stage == 1)
toReturn = $"{toReturn} sprout";
else if (stage == 2)
toReturn = $"{toReturn} sapling";
else if (stage == 3)
toReturn = $"{toReturn} bush";
else if (stage >= 4)
toReturn = $"{toReturn} tree";
if (fruitTree.fruitsOnTree.Value > 0)
toReturn = $"Harvestable {toReturn}";
return toReturn;
}
public static string getTree(Tree tree)
{
int treeType = tree.treeType.Value;
int treeStage = tree.growthStage.Value;
string treeName = "tree";
string seedName = "";
// Return with the name if it's one of the 3 special trees
switch (treeType)
{
case 4:
case 5:
return "Winter Tree";
case 6:
return "Palm Tree";
case 7:
return "Mushroom Tree";
}
if (treeType <= 3)
seedName = Game1.objectInformation[308 + treeType].Split('/')[0];
else if (treeType == 8)
seedName = Game1.objectInformation[292].Split('/')[0];
if (treeStage >= 1)
{
switch (seedName.ToLower())
{
case "mahogany seed":
treeName = "Mahogany";
break;
case "acorn":
treeName = "Oak";
break;
case "maple seed":
treeName = "Maple";
break;
case "pine cone":
treeName = "Pine";
break;
}
if (treeStage == 1)
treeName = $"{treeName} sprout";
else if (treeStage == 2)
treeName = $"{treeName} sapling";
else if (treeStage == 3 || treeStage == 4)
treeName = $"{treeName} bush";
else if (treeStage >= 5)
treeName = $"{treeName} tree";
return treeName;
}
return seedName;
}
public static (string?, CATEGORY) getObjectAtTile(int x, int y)
{
string? toReturn = null;
StardewValley.Object obj = Game1.currentLocation.getObjectAtTile(x, y);
int index = obj.ParentSheetIndex;
toReturn = obj.DisplayName;
// Get object names based on index
switch (index)
{
case 313:
case 314:
case 315:
case 316:
case 317:
case 318:
case 452:
case 674:
case 675:
case 676:
case 677:
case 678:
case 679:
case 750:
case 784:
case 785:
case 786:
return ("Weed", CATEGORY.Debris);
case 792:
case 793:
case 794:
return ("Fertile weed", CATEGORY.Debris);
case 319:
case 320:
case 321:
return ("Ice crystal", CATEGORY.Debris);
case 75:
return ("Geode", CATEGORY.MineItems);
case 32:
case 34:
case 36:
case 38:
case 40:
case 42:
case 48:
case 50:
case 52:
case 54:
case 56:
case 58:
return ("Coloured stone", CATEGORY.Debris);
case 668:
case 670:
case 845:
case 846:
case 847:
return ("Mine stone", CATEGORY.MineItems);
case 818:
return ("Clay stone", CATEGORY.Debris);
case 816:
case 817:
return ("Fossil stone", CATEGORY.Debris);
case 118:
case 120:
case 122:
case 124:
return ("Barrel", CATEGORY.MineItems);
case 119:
case 121:
case 123:
case 125:
return ("Item box", CATEGORY.MineItems);
}
if (Game1.currentLocation is Mine or MineShaft)
{
switch (index)
{
case 76:
return ("Frozen geode", CATEGORY.MineItems);
case 77:
return ("Magma geode", CATEGORY.MineItems);
case 8:
case 66:
return ("Amethyst node", CATEGORY.MineItems);
case 14:
case 62:
return ("Aquamarine node", CATEGORY.MineItems);
case 843:
case 844:
return ("Cinder shard node", CATEGORY.MineItems);
case 2:
case 72:
return ("Diamond node", CATEGORY.MineItems);
case 12:
case 60:
return ("Emerald node", CATEGORY.MineItems);
case 44:
return ("Gem node", CATEGORY.MineItems);
case 6:
case 70:
return ("Jade node", CATEGORY.MineItems);
case 46:
return ("Mystic stone", CATEGORY.MineItems);
case 74:
return ("Prismatic node", CATEGORY.MineItems);
case 4:
case 64:
return ("Ruby node", CATEGORY.MineItems);
case 10:
case 68:
return ("Topaz node", CATEGORY.MineItems);
case 819:
return ("Omni geode node", CATEGORY.MineItems);
case 751:
case 849:
return ("Copper node", CATEGORY.MineItems);
case 764:
return ("Gold node", CATEGORY.MineItems);
case 765:
return ("Iridium node", CATEGORY.MineItems);
case 290:
case 850:
return ("Iron node", CATEGORY.MineItems);
}
}
if (obj is Chest)
{
Chest chest = (Chest)obj;
return (chest.DisplayName, CATEGORY.Chests);
}
if (obj is Furniture)
return (toReturn, CATEGORY.Furnitures);
if (toReturn != null)
return (toReturn, CATEGORY.Others);
return (null, CATEGORY.Others);
}
public static bool isMineDownLadderAtTile(int x, int y)
{
try
{
if (Game1.currentLocation is Mine or MineShaft)
{
int? index = null;
if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null)
index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex;
if (index == 173 || index == 174)
return true;
}
}
catch (Exception) { }
return false;
}
public static bool isMineUpLadderAtTile(int x, int y)
{
try
{
if (Game1.currentLocation is Mine or MineShaft)
{
int? index = null;
if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null)
index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex;
if (index == 115)
return true;
}
}
catch (Exception) { }
return false;
}
public static bool isElevatorAtTile(int x, int y)
{
try
{
if (Game1.currentLocation is Mine or MineShaft)
{
int? index = null;
if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null)
index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex;
if (index == 112)
return true;
}
}
catch (Exception) { }
return false;
}
public static string? getDoorAtTile(int x, int y)
{
Point tilePoint = new Point(x, y);
StardewValley.Network.NetPointDictionary<string, Netcode.NetString> doorList = Game1.currentLocation.doors;
for (int i = 0; i < doorList.Count(); i++)
{
if (doorList.ContainsKey(tilePoint))
{
string? doorName;
doorList.TryGetValue(tilePoint, out doorName);
if (doorName != null)
return $"{doorName} door";
else
return "door";
}
}
return null;
}
public static string? getResourceClumpAtTile(int x, int y)
{
if (Game1.currentLocation is Woods)
return getStumpsInWoods(x, y);
for (int i = 0; i < Game1.currentLocation.resourceClumps.Count; i++)
{
if (Game1.currentLocation.resourceClumps[i].occupiesTile(x, y))
{
int index = Game1.currentLocation.resourceClumps[i].parentSheetIndex;
switch (index)
{
case 600:
return "Large Stump";
case 602:
return "Hollow Log";
case 622:
return "Meteorite";
case 752:
case 754:
case 756:
case 758:
return "Mine Rock";
case 672:
return "Boulder";
default:
return "Unknown";
}
}
}
return null;
}
public static string? getStumpsInWoods(int x, int y)
{
if (Game1.currentLocation is not Woods)
return null;
if ((x == 8 || x == 9) && y == 7)
{
return "Old Master Cannoli";
}
Netcode.NetObjectList<ResourceClump> stumps = ((Woods)Game1.currentLocation).stumps;
for (int i = 0; i < stumps.Count; i++)
{
if (stumps[i].occupiesTile(x, y))
{
return "Large Stump";
}
}
return null;
} }
} }
} }

View File

@@ -0,0 +1,512 @@
using System.Text.Json;
using StardewValley;
using static stardew_access.Features.Utils;
namespace stardew_access.Features
{
public class StaticTiles
{
// Static instance for the singleton pattern
private static StaticTiles? _instance;
/// <summary>
/// The singleton instance of the <see cref="StaticTiles"/> class.
/// </summary>
public static StaticTiles Instance
{
get
{
_instance ??= new StaticTiles();
return _instance;
}
}
/// <summary>
/// A nullable JsonElement containing static tile data.
/// </summary>
private static JsonElement? staticTilesData;
/// <summary>
/// A nullable JsonElement containing custom tile data.
/// </summary>
private static JsonElement? customTilesData;
/// <summary>
/// A dictionary that maps location names to tile data dictionaries for static tiles.
/// Each tile data dictionary maps tile coordinates (x, y) to a tuple containing the object name and category.
/// </summary>
private static Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> staticTilesDataDict = new();
/// <summary>
/// A dictionary that maps location names to tile data dictionaries for custom tiles.
/// Each tile data dictionary maps tile coordinates (x, y) to a tuple containing the object name and category.
/// </summary>
private static Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> customTilesDataDict = new();
/// <summary>
/// The file name of the JSON file containing static tile data.
/// </summary>
private const string StaticTilesFileName = "static-tiles.json";
/// <summary>
/// The file name of the JSON file containing custom tile data.
/// </summary>
private const string CustomTilesFileName = "custom-tiles.json";
/// <summary>
/// A dictionary that contains conditional lambda functions for checking specific game conditions.
/// Each lambda function takes two arguments: a conditionType (string) and a uniqueModId (string) and returns a boolean value.
/// </summary>
/// <remarks>
/// The following lambda functions are currently supported:
/// <list type="bullet">
/// <item>
/// <description>"Farm": Checks if the current in-game farm type matches the given farm type (conditionType).</description>
/// </item>
/// <item>
/// <description>"JojaMember": Checks if the player has the "JojaMember" mail. The input arguments are ignored.</description>
/// </item>
/// </list>
/// Additional lambda functions can be added as needed.
/// </remarks>
private static readonly Dictionary<string, Func<string, string, bool>> conditionals = new()
{
["Farm"] = (conditionType, uniqueModId) =>
{
if (string.IsNullOrEmpty(uniqueModId))
{
// Branch for vanilla locations
// Calculate farmTypeIndex using the switch expression
int farmTypeIndex = conditionType.ToLower() switch
{
"default" => 0,
"riverlands" => 1,
"forest" => 2,
"mountains" => 3,
"combat" => 4,
"fourcorners" => 5,
"beach" => 6,
_ => 7,
};
// Return true if the farmTypeIndex matches the current in-game farm type, otherwise false
return farmTypeIndex == Game1.whichFarm;
}
else
{
// Branch for mod locations
// Log an error message and return false, as mod locations are not yet supported for the Farm conditional
MainClass.ErrorLog("Mod locations are not yet supported for the Farm conditional.");
return false;
}
},
["JojaMember"] = (conditionType, uniqueModId) =>
{
// Return true if the player has the "JojaMember" mail, otherwise false
return Game1.MasterPlayer.mailReceived.Contains("JojaMember");
}
};
/// <summary>
/// Initializes a new instance of the <see cref="StaticTiles"/> class.
/// Loads the tile files and sets up the tile dictionaries.
/// </summary>
private StaticTiles()
{
LoadTilesFiles();
SetupTilesDicts();
}
/// <summary>
/// Loads the static and custom tile files.
/// </summary>
public static void LoadTilesFiles()
{
if (MainClass.ModHelper is null) return;
staticTilesData = LoadJsonFile(StaticTilesFileName);
customTilesData = LoadJsonFile(CustomTilesFileName);
}
/// <summary>
/// Adds a conditional lambda function to the conditionals dictionary at runtime.
/// </summary>
/// <param name="conditionName">The name of the condition to be added.</param>
/// <param name="conditionLambda">The lambda function to be added. It should accept two strings (conditionName and uniqueModID) and return a bool.</param>
/// <returns>Returns true if the lambda was added successfully, and false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if the conditionName or conditionLambda is null or empty.</exception>
public static bool AddConditionalLambda(string conditionName, Func<string, string, bool> conditionLambda)
{
// Check if the conditionName is not null or empty
if (string.IsNullOrEmpty(conditionName))
{
throw new ArgumentException("Condition name cannot be null or empty.", nameof(conditionName));
}
// Check if the conditionLambda is not null
if (conditionLambda == null)
{
throw new ArgumentException("Condition lambda cannot be null.", nameof(conditionLambda));
}
// Check if the conditionName already exists in the dictionary
if (conditionals.ContainsKey(conditionName))
{
MainClass.ErrorLog($"A conditional with the name '{conditionName}' already exists.");
return false;
}
// Add the lambda to the dictionary
conditionals.Add(conditionName, conditionLambda);
return true;
}
/// <summary>
/// Creates a location tile dictionary based on the given JSON dictionary.
/// </summary>
/// <param name="jsonDict">The JSON dictionary containing location tile data.</param>
/// <returns>A dictionary mapping tile coordinates to tile names and categories.</returns>
public static Dictionary<(short x, short y), (string name, CATEGORY category)> CreateLocationTileDict(JsonElement locationJson)
{
var jsonDict = locationJson.EnumerateObject().ToDictionary(p => p.Name, p => p.Value);
var locationData = new Dictionary<(short x, short y), (string name, CATEGORY category)>(jsonDict.Count);
// Iterate over the JSON dictionary
foreach (var item in jsonDict)
{
var name = item.Key;
// Error handling: Check if "x" and "y" properties exist in the JSON object
if (!item.Value.TryGetProperty("x", out var xElement) || !item.Value.TryGetProperty("y", out var yElement))
{
MainClass.ErrorLog($"Missing x or y property for {name}");
continue;
}
var xValues = xElement.EnumerateArray().Select(x => x.GetInt16()).ToArray();
var yValues = yElement.EnumerateArray().Select(y => y.GetInt16()).ToArray();
// Error handling: Ensure that x and y arrays are not empty
if (xValues.Length == 0 || yValues.Length == 0)
{
MainClass.ErrorLog($"Empty x or y array for {name}");
continue;
}
// Get the "type" property if it exists, otherwise use the default value "Others"
var type = item.Value.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : "Others";
// Obtain the category instance
var category = CATEGORY.FromString(type!);
// Iterate over y and x values, adding entries to the locationData dictionary
for (int j = 0; j < yValues.Length; j++)
{
var y = yValues[j];
for (int i = 0; i < xValues.Length; i++)
{
var x = xValues[i];
locationData.TryAdd((x, y), (name, category));
}
}
}
return locationData;
}
/// <summary>
/// Represents the different categories of locations.
/// </summary>
public enum LocationCategory
{
/// <summary>
/// Represents mod locations with conditional requirements.
/// </summary>
ModConditional,
/// <summary>
/// Represents mod locations without conditional requirements.
/// </summary>
Mod,
/// <summary>
/// Represents vanilla locations with conditional requirements.
/// </summary>
VanillaConditional,
/// <summary>
/// Represents vanilla locations without conditional requirements.
/// </summary>
Vanilla
}
/// <summary>
/// Determines the location category based on the given location name.
/// </summary>
/// <param name="name">The location name.</param>
/// <returns>The location category.</returns>
public static LocationCategory GetLocationCategory(string name)
{
bool hasDoubleUnderscore = name.Contains("__");
bool hasDoubleVerticalBar = name.Contains("||");
if (hasDoubleUnderscore && hasDoubleVerticalBar)
return LocationCategory.ModConditional;
if (hasDoubleVerticalBar)
return LocationCategory.Mod;
if (hasDoubleUnderscore)
return LocationCategory.VanillaConditional;
return LocationCategory.Vanilla;
}
/// <summary>
/// Sorts location data from a JsonElement into four dictionaries based on their type (mod conditional, mod, vanilla conditional, or vanilla).
/// </summary>
/// <param name="json">A JsonElement containing location data.</param>
/// <returns>
/// A tuple containing four dictionaries:
/// - modConditionalLocations: A dictionary of mod locations with conditionals.
/// - modLocations: A dictionary of mod locations without conditionals.
/// - vanillaConditionalLocations: A dictionary of vanilla locations with conditionals.
/// - vanillaLocations: A dictionary of vanilla locations without conditionals.
/// Each dictionary maps a location name to another dictionary, which maps tile coordinates (x, y) to a tuple containing the object name and category.
/// </returns>
/// <remarks>
/// This function iterates over the properties of the input JsonElement and categorizes each location based on the naming conventions.
/// If a location has a conditional, the function checks if the condition is met before adding it to the respective dictionary.
/// If a mod location is specified, the function checks if the mod is loaded before adding it to the respective dictionary.
/// </remarks>
public static (
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> modConditionalLocations,
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> modLocations,
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> vanillaConditionalLocations,
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> vanillaLocations
) SortLocationsByType(JsonElement json)
{
var modConditionalLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var modLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var vanillaConditionalLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var vanillaLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var categoryDicts = new Dictionary<LocationCategory, Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>>
{
{ LocationCategory.ModConditional, modConditionalLocations },
{ LocationCategory.Mod, modLocations },
{ LocationCategory.VanillaConditional, vanillaConditionalLocations },
{ LocationCategory.Vanilla, vanillaLocations }
};
foreach (var property in json.EnumerateObject())
{
if (property.Value.ValueKind != JsonValueKind.Object)
{
MainClass.ErrorLog($"Invalid value type for {property.Name}");
continue;
}
string propertyName = property.Name;
string uniqueModId = "";
var splitModId = propertyName.Split("||", StringSplitOptions.RemoveEmptyEntries);
if (splitModId.Length == 2)
{
propertyName = splitModId[0];
uniqueModId = splitModId[1];
if (MainClass.ModHelper == null || !MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModId))
{
continue;
}
}
var category = GetLocationCategory(propertyName);
if (category == LocationCategory.VanillaConditional || category == LocationCategory.ModConditional)
{
var splitPropertyName = propertyName.Split("__", StringSplitOptions.RemoveEmptyEntries);
if (splitPropertyName.Length == 2)
{
propertyName = splitPropertyName[0];
string conditionalName = splitPropertyName[1];
if (conditionals.TryGetValue(conditionalName, out var conditionalFunc))
{
if (!conditionalFunc(conditionalName, uniqueModId))
{
continue;
}
}
else
{
MainClass.ErrorLog($"Unknown conditional name: {conditionalName}");
continue;
}
}
}
var locationDict = CreateLocationTileDict(property.Value);
if (categoryDicts.TryGetValue(category, out var targetDict))
{
targetDict.Add(propertyName, locationDict);
}
else
{
MainClass.ErrorLog($"Unknown location category for {propertyName}");
}
}
return (modConditionalLocations, modLocations, vanillaConditionalLocations, vanillaLocations);
}
/// <summary>
/// Merges the contents of the source dictionary into the destination dictionary.
/// If a key exists in both dictionaries and the associated values are dictionaries, the function merges them recursively.
/// If the values are not dictionaries, the value from the source dictionary overwrites the value in the destination dictionary.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionaries.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionaries.</typeparam>
/// <param name="destinationDictionary">The destination dictionary to merge the source dictionary into.</param>
/// <param name="sourceDictionary">The source dictionary containing the data to merge into the destination dictionary.</param>
private static void MergeDicts<TKey, TValue>(
Dictionary<TKey, TValue> destinationDictionary,
Dictionary<TKey, TValue> sourceDictionary) where TKey : notnull
{
if (destinationDictionary == null || sourceDictionary == null)
{
// Log a warning or throw an exception if either dictionary is null
return;
}
foreach (var sourceEntry in sourceDictionary)
{
// Try to get the existing value from the destination dictionary
if (destinationDictionary.TryGetValue(sourceEntry.Key, out var existingValue))
{
// If both existing value and the source value are dictionaries,
// merge them recursively
if (existingValue is Dictionary<TKey, TValue> existingDictionary
&& sourceEntry.Value is Dictionary<TKey, TValue> sourceSubDictionary)
{
MergeDicts(existingDictionary, sourceSubDictionary);
}
else
{
// Overwrite the existing value if it's not a dictionary
destinationDictionary[sourceEntry.Key] = sourceEntry.Value;
}
}
else
{
// Add a new entry if the key doesn't exist in the destination dictionary
destinationDictionary[sourceEntry.Key] = sourceEntry.Value;
}
}
}
/// <summary>
/// Builds a dictionary containing location data and tile information based on the provided JsonElement.
/// </summary>
/// <param name="json">A JsonElement containing the location and tile data.</param>
/// <returns>A dictionary containing location data and tile information.</returns>
public static Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> BuildTilesDict(JsonElement json)
{
// Sort the locations by their types (modConditional, mod, vanillaConditional, vanilla)
var (modConditionalLocations, modLocations, vanillaConditionalLocations, vanillaLocations) = SortLocationsByType(json);
// Create a merged dictionary to store all the location dictionaries
var mergedDict = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>(StringComparer.OrdinalIgnoreCase);
// Merge each category-specific dictionary into the merged dictionary. Prioritize conditional locations whose conditions are true and mod locations where the corresponding mod is loaded. Overwrite their default and vanilla versions, respectively.
MergeDicts(mergedDict, modConditionalLocations);
MergeDicts(mergedDict, modLocations);
MergeDicts(mergedDict, vanillaConditionalLocations);
MergeDicts(mergedDict, vanillaLocations);
return mergedDict;
}
/// <summary>
/// Sets up the tile dictionaries (staticTilesDataDict and customTilesDataDict) using the data from the loaded JsonElements.
/// </summary>
public static void SetupTilesDicts()
{
if (staticTilesData.HasValue && staticTilesData.Value.ValueKind != JsonValueKind.Undefined)
{
staticTilesDataDict = BuildTilesDict(staticTilesData.Value);
}
else
{
staticTilesDataDict = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
}
if (customTilesData.HasValue && customTilesData.Value.ValueKind != JsonValueKind.Undefined)
{
customTilesDataDict = BuildTilesDict(customTilesData.Value);
}
else
{
customTilesDataDict = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
}
}
/// <summary>
/// Retrieves the tile information (name and optionally category) from the dictionaries based on the specified location and coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="currentLocationName">The name of the current location. Defaults to Game1.currentLocation.Name.</param>
/// <param name="includeCategory">Specifies whether to include the tile's category in the returned tuple.</param>
/// <returns>A tuple containing the tile's name and optionally its category. If the tile is not found, the name will be null and the category will be CATEGORY.Others if requested.</returns>
private static (string? name, CATEGORY? category) GetTileInfoAt(int x, int y, string? currentLocationName = null, bool includeCategory = false)
{
currentLocationName ??= Game1.currentLocation.Name;
if (customTilesDataDict != null && customTilesDataDict.TryGetValue(currentLocationName, out var customLocationDict))
{
if (customLocationDict != null && customLocationDict.TryGetValue(((short)x, (short)y), out var customTile))
{
return (customTile.name, includeCategory ? customTile.category : (CATEGORY?)null);
}
}
if (staticTilesDataDict != null && staticTilesDataDict.TryGetValue(currentLocationName, out var staticLocationDict))
{
if (staticLocationDict != null && staticLocationDict.TryGetValue(((short)x, (short)y), out var staticTile))
{
return (staticTile.name, includeCategory ? staticTile.category : (CATEGORY?)null);
}
}
return (null, includeCategory ? CATEGORY.Others : (CATEGORY?)null);
}
/// <summary>
/// Retrieves the tile name from the dictionaries based on the specified location and coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="currentLocationName">The name of the current location. Defaults to Game1.currentLocation.Name.</param>
/// <returns>The name of the tile if found, or null if not found.</returns>
public static string GetStaticTileNameAt(int x, int y, string? currentLocationName = null)
{
var (name, _) = GetTileInfoAt(x, y, currentLocationName, includeCategory: false);
return name ?? "";
}
/// <summary>
/// Retrieves the tile information (name and category) from the dictionaries based on the specified location and coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="currentLocationName">The name of the current location. Defaults to Game1.currentLocation.Name.</param>
/// <returns>A tuple containing the tile's name and category. If the tile is not found, the name will be null and the category will be CATEGORY.Others.</returns>
public static (string? name, CATEGORY category) GetStaticTileInfoAtWithCategory(int x, int y, string? currentLocationName = null)
{
var (name, category) = GetTileInfoAt(x, y, currentLocationName, includeCategory: true);
return (name, category ?? CATEGORY.Others);
}
}
}

View File

@@ -0,0 +1,953 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Locations;
using StardewValley.Objects;
using StardewValley.TerrainFeatures;
using System.Text;
namespace stardew_access.Features
{
public class TileInfo
{
private static readonly string[] trackable_machines = { "bee house", "cask", "press", "keg", "machine", "maker", "preserves jar", "bone mill", "kiln", "crystalarium", "furnace", "geode crusher", "tapper", "lightning rod", "incubator", "wood chipper", "worm bin", "loom", "statue of endless fortune", "statue of perfection", "crab pot" };
private static readonly Dictionary<int, string> ResourceClumpNames = new()
{
{ 600, "Large Stump" },
{ 602, "Hollow Log" },
{ 622, "Meteorite" },
{ 672, "Boulder" },
{ 752, "Mine Rock" },
{ 754, "Mine Rock" },
{ 756, "Mine Rock" },
{ 758, "Mine Rock" },
{ 190, "Giant Cauliflower" },
{ 254, "Giant Melon" },
{ 276, "Giant Pumpkin" }
};
///<summary>Returns the name of the object at tile alongwith it's category's name</summary>
public static (string? name, string? categoryName) getNameWithCategoryNameAtTile(Vector2 tile, GameLocation? currentLocation)
{
(string? name, CATEGORY? category) = getNameWithCategoryAtTile(tile, currentLocation);
category ??= CATEGORY.Others;
return (name, category.ToString());
}
///<summary>Returns the name of the object at tile</summary>
public static string? GetNameAtTile(Vector2 tile, GameLocation? currentLocation = null)
{
currentLocation ??= Game1.currentLocation;
return getNameWithCategoryAtTile(tile, currentLocation).name;
}
///<summary>Returns the name of the object at tile alongwith it's category</summary>
public static (string? name, CATEGORY? category) getNameWithCategoryAtTile(Vector2 tile, GameLocation? currentLocation, bool lessInfo = false)
{
currentLocation ??= Game1.currentLocation;
int x = (int)tile.X;
int y = (int)tile.Y;
var terrainFeature = currentLocation.terrainFeatures.FieldDict;
if (currentLocation.isCharacterAtTile(tile) is NPC npc)
{
CATEGORY category = npc.isVillager() || npc.CanSocialize ? CATEGORY.Farmers : CATEGORY.NPCs;
return (npc.displayName, category);
}
string? farmAnimal = getFarmAnimalAt(currentLocation, x, y);
if (farmAnimal is not null)
{
return (farmAnimal, CATEGORY.FarmAnimals);
}
(string? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y, currentLocation.Name);
if (staticTile.name != null)
{
return (staticTile.name, staticTile.category);
}
(string? name, CATEGORY? category) dynamicTile = DynamicTiles.GetDynamicTileAt(currentLocation, x, y, lessInfo);
if (dynamicTile.name != null)
{
return (dynamicTile.name, dynamicTile.category);
}
if (currentLocation.isObjectAtTile(x, y))
{
(string? name, CATEGORY? category) obj = getObjectAtTile(currentLocation, x, y, lessInfo);
return (obj.name, obj.category);
}
if (currentLocation.isWaterTile(x, y) && !lessInfo && IsCollidingAtTile(currentLocation, x, y))
{
return ("Water", CATEGORY.WaterTiles);
}
string? resourceClump = getResourceClumpAtTile(currentLocation, x, y, lessInfo);
if (resourceClump != null)
{
return (resourceClump, CATEGORY.ResourceClumps);
}
if (terrainFeature.TryGetValue(tile, out var tf))
{
(string? name, CATEGORY category) terrain = getTerrainFeatureAtTile(tf);
if (terrain.name != null)
{
return (terrain.name, terrain.category);
}
}
string? bush = GetBushAtTile(currentLocation, x, y, lessInfo);
if (bush != null)
{
return (bush, CATEGORY.Bush);
}
string? door = getDoorAtTile(currentLocation, x, y);
string? warp = getWarpPointAtTile(currentLocation, x, y);
if (warp != null || door != null)
{
return (warp ?? door, CATEGORY.Doors);
}
if (isMineDownLadderAtTile(currentLocation, x, y))
{
return ("Ladder", CATEGORY.Doors);
}
if (isShaftAtTile(currentLocation, x, y))
{
return ("Shaft", CATEGORY.Doors);
}
if (isMineUpLadderAtTile(currentLocation, x, y))
{
return ("Up Ladder", CATEGORY.Doors);
}
if (isElevatorAtTile(currentLocation, x, y))
{
return ("Elevator", CATEGORY.Doors);
}
string? junimoBundle = GetJunimoBundleAt(currentLocation, x, y);
if (junimoBundle != null)
{
return (junimoBundle, CATEGORY.JunimoBundle);
}
// Track dropped items
if (MainClass.Config.TrackDroppedItems)
{
try
{
foreach (var item in currentLocation.debris)
{
int xPos = ((int)item.Chunks[0].position.Value.X / Game1.tileSize) + 1;
int yPos = ((int)item.Chunks[0].position.Value.Y / Game1.tileSize) + 1;
if (xPos != x || yPos != y || item.item == null) continue;
string name = item.item.DisplayName;
int count = item.item.Stack;
return ($"Dropped Item: {count} {name}", CATEGORY.DroppedItems);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occurred while detecting dropped items:\n{e.Message}");
}
}
return (null, CATEGORY.Others);
}
/// <summary>
/// Gets the bush at the specified tile coordinates in the provided GameLocation.
/// </summary>
/// <param name="currentLocation">The GameLocation instance to search for bushes.</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <param name="lessInfo">Whether to return less information about the bush.</param>
/// <returns>A string describing the bush if one is found at the specified coordinates, otherwise null.</returns>
public static string? GetBushAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
Bush? bush = (Bush)currentLocation.getLargeTerrainFeatureAt(x, y);
if (bush is null || (lessInfo && (bush.tilePosition.Value.X != x || bush.tilePosition.Value.Y != y)))
return null;
if (!bush.townBush.Value && bush.tileSheetOffset.Value == 1 && bush.inBloom(Game1.GetSeasonForLocation(currentLocation), Game1.dayOfMonth))
{
string season = bush.overrideSeason.Value == -1 ? Game1.GetSeasonForLocation(currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason.Value);
int shakeOff = season switch
{
"spring" => 296,
"fall" => 410,
_ => -1
};
shakeOff = bush.size.Value switch
{
3 => 815,
4 => 73,
_ => shakeOff
};
if (shakeOff == -1)
{
return null;
}
return bush.townBush.Value
? "Harvestable Town Bush"
: bush.greenhouseBush.Value
? "Harvestable Greenhouse Bush"
: "Harvestable Bush";
}
return bush.townBush.Value
? "Town Bush"
: bush.greenhouseBush.Value
? "Greenhouse Bush"
: "Bush";
}
/// <summary>
/// Determines if there is a Junimo bundle at the specified tile coordinates in the provided GameLocation.
/// </summary>
/// <param name="currentLocation">The GameLocation instance to search for Junimo bundles.</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <returns>The name of the Junimo bundle if one is found at the specified coordinates, otherwise null.</returns>
public static string? GetJunimoBundleAt(GameLocation currentLocation, int x, int y)
{
if (currentLocation is CommunityCenter communityCenter)
{
// Determine the name of the bundle based on the tile coordinates
string? name = (x, y) switch
{
(14, 5) => "Pantry",
(14, 23) => "Crafts Room",
(40, 10) => "Fish Tank",
(63, 14) => "Boiler Room",
(55, 6) => "Vault",
(46, 12) => "Bulletin Board",
_ => null,
};
// If a bundle name is found and a note should appear in the area, return the bundle name
if (name is not null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name)))
return $"{name} bundle";
}
else if (currentLocation is AbandonedJojaMart)
{
// Determine the name of the bundle based on the tile coordinates
string? name = (x, y) switch
{
(8, 8) => "Missing",
_ => null,
};
if (name is not null)
// Bundle name was found
return $"{name} bundle";
}
// No bundle was found
return null;
}
/// <summary>
/// Determines if there is a collision at the specified tile coordinates in the provided GameLocation.
/// </summary>
/// <param name="currentLocation">The GameLocation instance to search for collisions.</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <returns>True if a collision is detected at the specified tile coordinates, otherwise False.</returns>
public static bool IsCollidingAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
// This function highly optimized over readability because `currentLocation.isCollidingPosition` takes ~30ms on the Farm map, more on larger maps I.E. Forest.
// Return the result of the logical comparison directly, inlining operations
// Check if the tile is NOT a warp point and if it collides with an object or terrain feature
// OR if the tile has stumps in a Woods location
return !isWarpPointAtTile(currentLocation, x, y) &&
(currentLocation.isCollidingPosition(new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62), Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true) ||
(currentLocation is Woods woods && getStumpsInWoods(woods, x, y, lessInfo) is not null));
}
/// <summary>
/// Returns the Warp object at the specified tile coordinates or null if not found.
/// </summary>
private static Warp? GetWarpAtTile(GameLocation currentLocation, int x, int y)
{
if (currentLocation is null) return null;
int warpsCount = currentLocation.warps.Count;
for (int i = 0; i < warpsCount; i++)
{
if (currentLocation.warps[i].X == x && currentLocation.warps[i].Y == y)
return currentLocation.warps[i];
}
return null;
}
/// <summary>
/// Returns the name of the warp point at the specified tile coordinates, or null if not found.
/// </summary>
public static string? getWarpPointAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
Warp? warpPoint = GetWarpAtTile(currentLocation, x, y);
if (warpPoint != null)
{
return lessInfo ? warpPoint.TargetName : $"{warpPoint.TargetName} Entrance";
}
return null;
}
/// <summary>
/// Returns true if there's a warp point at the specified tile coordinates, or false otherwise.
/// </summary>
public static bool isWarpPointAtTile(GameLocation currentLocation, int x, int y)
{
return GetWarpAtTile(currentLocation, x, y) != null;
}
/// <summary>
/// Gets the farm animal at the specified tile coordinates in the given location.
/// </summary>
/// <param name="location">The location where the farm animal might be found. Must be either a Farm or an AnimalHouse (coop, barn, etc).</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <returns>
/// A string containing the farm animal's name, type, and age if a farm animal is found at the specified tile;
/// null if no farm animal is found or if the location is not a Farm or an AnimalHouse.
/// </returns>
public static string? getFarmAnimalAt(GameLocation location, int x, int y)
{
// Return null if the location is null or not a Farm or AnimalHouse
if (location is null || !(location is Farm || location is AnimalHouse))
return null;
// Use an empty enumerable to store farm animals if no animals are found
IEnumerable<FarmAnimal> farmAnimals = Enumerable.Empty<FarmAnimal>();
// If the location is a Farm, get all the farm animals
if (location is Farm farm)
farmAnimals = farm.getAllFarmAnimals();
// If the location is an AnimalHouse, get all the animals from the AnimalHouse
else if (location is AnimalHouse animalHouse)
farmAnimals = animalHouse.animals.Values;
// Use LINQ to find the first farm animal at the specified tile (x, y) coordinates
var foundAnimal = farmAnimals.FirstOrDefault(farmAnimal => farmAnimal.getTileX() == x && farmAnimal.getTileY() == y);
// If a farm animal was found at the specified tile coordinates
if (foundAnimal != null)
{
string name = foundAnimal.displayName;
int age = foundAnimal.age.Value;
string type = foundAnimal.displayType;
// Return a formatted string with the farm animal's name, type, and age
return $"{name}, {type}, age {age}";
}
// If no farm animal was found, return null
return null;
}
/// <summary>
/// Retrieves the name and category of the terrain feature at the given tile.
/// </summary>
/// <param name="terrain">A reference to the terrain feature to be checked.</param>
/// <returns>A tuple containing the name and category of the terrain feature at the tile.</returns>
public static (string? name, CATEGORY category) getTerrainFeatureAtTile(Netcode.NetRef<TerrainFeature> terrain)
{
// Get the terrain feature from the reference
var terrainFeature = terrain.Get();
// Check if the terrain feature is HoeDirt
if (terrainFeature is HoeDirt dirt)
{
return (getHoeDirtDetail(dirt), CATEGORY.Crops);
}
// Check if the terrain feature is a CosmeticPlant
else if (terrainFeature is CosmeticPlant cosmeticPlant)
{
string toReturn = cosmeticPlant.textureName().ToLower();
toReturn = toReturn.Replace("terrain", "").Replace("feature", "");
return (toReturn, CATEGORY.Furnitures);
}
// Check if the terrain feature is Flooring
else if (terrainFeature is Flooring flooring && MainClass.Config.ReadFlooring)
{
bool isPathway = flooring.isPathway.Get();
bool isSteppingStone = flooring.isSteppingStone.Get();
string toReturn = isPathway ? "Pathway" : (isSteppingStone ? "Stepping Stone" : "Flooring");
return (toReturn, CATEGORY.Flooring);
}
// Check if the terrain feature is a FruitTree
else if (terrainFeature is FruitTree fruitTree)
{
return (getFruitTree(fruitTree), CATEGORY.Trees);
}
// Check if the terrain feature is Grass
else if (terrainFeature is Grass)
{
return ("Grass", CATEGORY.Debris);
}
// Check if the terrain feature is a Tree
else if (terrainFeature is Tree tree)
{
return (getTree(tree), CATEGORY.Trees);
}
return (null, CATEGORY.Others);
}
/// <summary>
/// Retrieves a detailed description of HoeDirt, including its soil, plant, and other relevant information.
/// </summary>
/// <param name="dirt">The HoeDirt object to get details for.</param>
/// <param name="ignoreIfEmpty">If true, the method will return an empty string for empty soil; otherwise, it will return "Soil".</param>
/// <returns>A string representing the details of the provided HoeDirt object.</returns>
public static string getHoeDirtDetail(HoeDirt dirt, bool ignoreIfEmpty = false)
{
// Use StringBuilder for efficient string manipulation
StringBuilder detail = new();
// Calculate isWatered and isFertilized only once
bool isWatered = dirt.state.Value == HoeDirt.watered;
bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer;
// Check the watered status and append it to the detail string
if (isWatered && MainClass.Config.WateredToggle)
detail.Append("Watered ");
else if (!isWatered && !MainClass.Config.WateredToggle)
detail.Append("Unwatered ");
// Check if the dirt is fertilized and append it to the detail string
if (isFertilized)
detail.Append("Fertilized ");
// Check if the dirt has a crop
if (dirt.crop != null)
{
// Handle forage crops
if (dirt.crop.forageCrop.Value)
{
detail.Append(dirt.crop.whichForageCrop.Value switch
{
1 => "Spring onion",
2 => "Ginger",
_ => "Forageable crop"
});
}
else // Handle non-forage crops
{
// Append the crop name to the detail string
string cropName = Game1.objectInformation[dirt.crop.indexOfHarvest.Value].Split('/')[0];
detail.Append(cropName);
// Check if the crop is harvestable and prepend it to the detail string
if (dirt.readyForHarvest())
detail.Insert(0, "Harvestable ");
// Check if the crop is dead and prepend it to the detail string
if (dirt.crop.dead.Value)
detail.Insert(0, "Dead ");
}
}
else if (!ignoreIfEmpty) // If there's no crop and ignoreIfEmpty is false, append "Soil" to the detail string
{
detail.Append("Soil");
}
return detail.ToString();
}
/// <summary>
/// Retrieves the fruit tree's display name based on its growth stage and fruit index.
/// </summary>
/// <param name="fruitTree">The FruitTree object to get the name for.</param>
/// <returns>The fruit tree's display name.</returns>
public static string getFruitTree(FruitTree fruitTree)
{
int stage = fruitTree.growthStage.Value;
int fruitIndex = fruitTree.indexOfFruit.Get();
// Get the base name of the fruit tree from the object information
string toReturn = Game1.objectInformation[fruitIndex].Split('/')[0];
// Append the growth stage description to the fruit tree name
if (stage == 0)
toReturn = $"{toReturn} seed";
else if (stage == 1)
toReturn = $"{toReturn} sprout";
else if (stage == 2)
toReturn = $"{toReturn} sapling";
else if (stage == 3)
toReturn = $"{toReturn} bush";
else if (stage >= 4)
toReturn = $"{toReturn} tree";
// If there are fruits on the tree, prepend "Harvestable" to the name
if (fruitTree.fruitsOnTree.Value > 0)
toReturn = $"Harvestable {toReturn}";
return toReturn;
}
/// <summary>
/// Retrieves the tree's display name based on its type and growth stage.
/// </summary>
/// <param name="tree">The Tree object to get the name for.</param>
/// <returns>The tree's display name.</returns>
public static string getTree(Tree tree)
{
int treeType = tree.treeType.Value;
int treeStage = tree.growthStage.Value;
string seedName = "";
// Handle special tree types and return their names
switch (treeType)
{
case 4:
case 5:
return "Winter Tree";
case 6:
return "Palm Tree";
case 7:
return "Mushroom Tree";
}
// Get the seed name for the tree type
if (treeType <= 3)
seedName = Game1.objectInformation[308 + treeType].Split('/')[0];
else if (treeType == 8)
seedName = Game1.objectInformation[292].Split('/')[0];
// Determine the tree name and growth stage description
if (treeStage >= 1)
{
string treeName;
switch (seedName.ToLower())
{
case "mahogany seed":
treeName = "Mahogany";
break;
case "acorn":
treeName = "Oak";
break;
case "maple seed":
treeName = "Maple";
break;
case "pine cone":
treeName = "Pine";
break;
default:
treeName = "Coconut";
break;
}
// Append the growth stage description to the tree name
if (treeStage == 1)
treeName = $"{treeName} sprout";
else if (treeStage == 2)
treeName = $"{treeName} sapling";
else if (treeStage == 3 || treeStage == 4)
treeName = $"{treeName} bush";
else if (treeStage >= 5)
treeName = $"{treeName} tree";
return treeName;
}
// Return the seed name if the tree is at stage 0
return seedName;
}
#region Objects
/// <summary>
/// Retrieves the name and category of an object at a specific tile in the game location.
/// </summary>
/// <param name="currentLocation">The current game location.</param>
/// <param name="x">The X coordinate of the tile.</param>
/// <param name="y">The Y coordinate of the tile.</param>
/// <param name="lessInfo">An optional parameter to display less information, set to false by default.</param>
/// <returns>A tuple containing the object's name and category.</returns>
public static (string? name, CATEGORY category) getObjectAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
(string? name, CATEGORY category) toReturn = (null, CATEGORY.Others);
// Get the object at the specified tile
StardewValley.Object obj = currentLocation.getObjectAtTile(x, y);
if (obj == null) return toReturn;
int index = obj.ParentSheetIndex;
toReturn.name = obj.DisplayName;
// Get object names and categories based on index
(string? name, CATEGORY category) correctNameAndCategory = getCorrectNameAndCategoryFromIndex(index, obj.Name);
// Check the object type and assign the appropriate name and category
if (obj is Chest chest)
{
toReturn = (chest.DisplayName, CATEGORY.Containers);
}
else if (obj is IndoorPot indoorPot)
{
toReturn.name = $"{obj.DisplayName}, {getHoeDirtDetail(indoorPot.hoeDirt.Value, true)}";
}
else if (obj is Sign sign && sign.displayItem.Value != null)
{
toReturn.name = $"{sign.DisplayName}, {sign.displayItem.Value.DisplayName}";
}
else if (obj is Furniture furniture)
{
if (lessInfo && (furniture.TileLocation.X != x || furniture.TileLocation.Y != y))
{
toReturn.category = CATEGORY.Others;
toReturn.name = null;
}
else
{
toReturn.category = CATEGORY.Furnitures;
}
}
else if (obj.IsSprinkler() && obj.heldObject.Value != null) // Detect the upgrade attached to the sprinkler
{
string heldObjectName = obj.heldObject.Value.Name;
if (MainClass.ModHelper is not null)
{
if (heldObjectName.Contains("pressure nozzle", StringComparison.OrdinalIgnoreCase))
{
toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.pressurenozzle", new { value = toReturn.name });
}
else if (heldObjectName.Contains("enricher", StringComparison.OrdinalIgnoreCase))
{
toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.enricher", new { value = toReturn.name });
}
else
{
toReturn.name = $"{obj.heldObject.Value.DisplayName} {toReturn.name}";
}
}
}
else if ((obj.Type == "Crafting" && obj.bigCraftable.Value) || obj.Name.Equals("crab pot", StringComparison.OrdinalIgnoreCase))
{
foreach (string machine in trackable_machines)
{
if (obj.Name.Contains(machine, StringComparison.OrdinalIgnoreCase))
{
toReturn.name = obj.DisplayName;
toReturn.category = CATEGORY.Machines;
}
}
}
else if (correctNameAndCategory.name != null)
{
toReturn = correctNameAndCategory;
}
else if (obj.name.Equals("stone", StringComparison.OrdinalIgnoreCase))
toReturn.category = CATEGORY.Debris;
else if (obj.name.Equals("twig", StringComparison.OrdinalIgnoreCase))
toReturn.category = CATEGORY.Debris;
else if (obj.name.Contains("quartz", StringComparison.OrdinalIgnoreCase))
toReturn.category = CATEGORY.MineItems;
else if (obj.name.Contains("earth crystal", StringComparison.OrdinalIgnoreCase))
toReturn.category = CATEGORY.MineItems;
else if (obj.name.Contains("frozen tear", StringComparison.OrdinalIgnoreCase))
toReturn.category = CATEGORY.MineItems;
if (toReturn.category == CATEGORY.Machines) // Fix for `Harvestable table` and `Busy nodes`
{
MachineState machineState = GetMachineState(obj);
if (machineState == MachineState.Ready)
toReturn.name = $"Harvestable {toReturn.name}";
else if (machineState == MachineState.Busy)
toReturn.name = $"Busy {toReturn.name}";
}
return toReturn;
}
/// <summary>
/// Determines the state of a machine based on its properties.
/// </summary>
/// <param name="machine">The machine object to get the state of.</param>
/// <returns>A MachineState enumeration value representing the machine's state.</returns>
private static MachineState GetMachineState(StardewValley.Object machine)
{
// Check if the machine is a CrabPot and determine its state based on bait and heldObject
if (machine is CrabPot crabPot)
{
bool hasBait = crabPot.bait.Value is not null;
bool hasHeldObject = crabPot.heldObject.Value is not null;
if (hasBait && !hasHeldObject)
return MachineState.Busy;
else if (hasBait && hasHeldObject)
return MachineState.Ready;
}
// For other machine types, determine the state based on readyForHarvest, MinutesUntilReady, and heldObject
return GetMachineState(machine.readyForHarvest.Value, machine.MinutesUntilReady, machine.heldObject.Value);
}
/// <summary>
/// Determines the state of a machine based on its readiness for harvest, remaining minutes, and held object.
/// </summary>
/// <param name="readyForHarvest">A boolean indicating if the machine is ready for harvest.</param>
/// <param name="minutesUntilReady">The number of minutes remaining until the machine is ready.</param>
/// <param name="heldObject">The object held by the machine, if any.</param>
/// <returns>A MachineState enumeration value representing the machine's state.</returns>
private static MachineState GetMachineState(bool readyForHarvest, int minutesUntilReady, StardewValley.Object? heldObject)
{
// Determine the machine state based on the input parameters
if (readyForHarvest || (heldObject is not null && minutesUntilReady <= 0))
{
return MachineState.Ready;
}
else if (minutesUntilReady > 0)
{
return MachineState.Busy;
}
else
{
return MachineState.Waiting;
}
}
/// <summary>
/// Retrieves the correct name and category for an object based on its index and name.
/// </summary>
/// <param name="index">The object's index value.</param>
/// <param name="objName">The object's name.</param>
/// <returns>A tuple containing the object's correct name and category.</returns>
private static (string? name, CATEGORY category) getCorrectNameAndCategoryFromIndex(int index, string objName)
{
// Check the index for known cases and return the corresponding name and category
switch (index)
{
case 313:
case 314:
case 315:
case 316:
case 317:
case 318:
case 452:
case 674:
case 675:
case 676:
case 677:
case 678:
case 679:
case 750:
case 784:
case 785:
case 786:
return ("Weed", CATEGORY.Debris);
case 792:
case 793:
case 794:
return ("Fertile weed", CATEGORY.Debris);
case 319:
case 320:
case 321:
return ("Ice crystal", CATEGORY.Debris);
case 118:
case 120:
case 122:
case 124:
return ("Barrel", CATEGORY.MineItems);
case 119:
case 121:
case 123:
case 125:
return ("Item box", CATEGORY.MineItems);
}
// Check if the object name contains "stone" and handle specific cases based on index
if (objName.Contains("stone", StringComparison.OrdinalIgnoreCase))
{
// Return the corresponding name and category for specific stone-related objects
switch (index)
{
case 76:
return ("Frozen geode", CATEGORY.MineItems);
// ... (include other cases)
case 290:
case 850:
return ("Iron node", CATEGORY.MineItems);
}
}
// Return null for the name and the Others category if no match is found
return (null, CATEGORY.Others);
}
#endregion
/// <summary>
/// Check if a tile with the specified index exists at the given coordinates in the specified location.
/// </summary>
/// <param name="currentLocation">The current game location.</param>
/// <param name="x">The X coordinate of the tile.</param>
/// <param name="y">The Y coordinate of the tile.</param>
/// <param name="targetTileIndex">The target tile index to check for.</param>
/// <returns>True if a tile with the specified index exists at the given coordinates, false otherwise.</returns>
private static bool CheckTileIndex(GameLocation currentLocation, int x, int y, int targetTileIndex)
{
var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y];
return tile != null && tile.TileIndex == targetTileIndex;
}
/// <summary>
/// Determines if a mine down ladder is present at the specified tile location.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <returns>True if a mine down ladder is found at the specified tile, otherwise false.</returns>
public static bool isMineDownLadderAtTile(GameLocation currentLocation, int x, int y)
{
return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave"
? CheckTileIndex(currentLocation, x, y, 173)
: false;
}
/// <summary>
/// Determines if a mine shaft is present at the specified tile location.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <returns>True if a mine shaft is found at the specified tile, otherwise false.</returns>
public static bool isShaftAtTile(GameLocation currentLocation, int x, int y)
{
return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave"
? CheckTileIndex(currentLocation, x, y, 174)
: false;
}
/// <summary>
/// Determines if a mine up ladder is present at the specified tile location.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <returns>True if a mine up ladder is found at the specified tile, otherwise false.</returns>
public static bool isMineUpLadderAtTile(GameLocation currentLocation, int x, int y)
{
return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave"
? CheckTileIndex(currentLocation, x, y, 115)
: false;
}
/// <summary>
/// Determines if an elevator is present at the specified tile location.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <returns>True if an elevator is found at the specified tile, otherwise false.</returns>
public static bool isElevatorAtTile(GameLocation currentLocation, int x, int y)
{
return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave"
? CheckTileIndex(currentLocation, x, y, 112)
: false;
}
/// <summary>
/// Gets the door information at the specified tile coordinates in the given location.
/// </summary>
/// <param name="currentLocation">The GameLocation where the door might be found.</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <returns>A string containing the door information if a door is found at the specified tile; null if no door is found.</returns>
public static string? getDoorAtTile(GameLocation currentLocation, int x, int y)
{
// Create a Point object from the given tile coordinates
Point tilePoint = new(x, y);
// Access the doorList in the current location
StardewValley.Network.NetPointDictionary<string, Netcode.NetString> doorList = currentLocation.doors;
// Check if the doorList contains the given tile point
if (doorList.TryGetValue(tilePoint, out string? doorName))
{
// Return the door information with the name if available, otherwise use "door"
return doorName != null ? $"{doorName} door" : "door";
}
// No matching door found
return null;
}
/// <summary>
/// Gets the resource clump information at the specified tile coordinates in the given location.
/// </summary>
/// <param name="currentLocation">The GameLocation where the resource clump might be found.</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A string containing the resource clump information if a resource clump is found at the specified tile; null if no resource clump is found.</returns>
public static string? getResourceClumpAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
// Check if the current location is Woods and handle stumps in woods separately
if (currentLocation is Woods woods)
return getStumpsInWoods(woods, x, y, lessInfo);
// Iterate through resource clumps in the location using a for loop for performance reasons
for (int i = 0, count = currentLocation.resourceClumps.Count; i < count; i++)
{
var resourceClump = currentLocation.resourceClumps[i];
// Check if the resource clump occupies the tile and meets the lessInfo condition
if (resourceClump.occupiesTile(x, y) && (!lessInfo || (resourceClump.tile.X == x && resourceClump.tile.Y == y)))
{
// Get the resource clump name if available, otherwise use "Unknown"
return ResourceClumpNames.TryGetValue(resourceClump.parentSheetIndex.Value, out string? resourceName) ? resourceName : "Unknown";
}
}
// No matching resource clump found
return null;
}
/// <summary>
/// Gets the stump information at the specified tile coordinates in the given Woods location.
/// </summary>
/// <param name="woods">The Woods location where the stump might be found.</param>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the stump's origin. Default is false.</param>
/// <returns>A string containing the stump information if a stump is found at the specified tile; null if no stump is found.</returns>
public static string? getStumpsInWoods(Woods woods, int x, int y, bool lessInfo = false)
{
// Iterate through stumps in the Woods location
foreach (var stump in woods.stumps)
{
// Check if the stump occupies the tile and meets the lessInfo condition
if (stump.occupiesTile(x, y) && (!lessInfo || (stump.tile.X == x && stump.tile.Y == y)))
{
// Return stump information
return "Large Stump";
}
}
// No matching stump found
return null;
}
}
}

View File

@@ -0,0 +1,292 @@
using Microsoft.Xna.Framework;
using xTile;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Features
{
/// <summary>
/// Allows browsing of the map and snapping mouse to tiles with the arrow keys
/// </summary>
public class TileViewer
{
//None of these positions take viewport into account; other functions are responsible later
private Vector2 ViewingOffset = Vector2.Zero;
private Vector2 relativeOffsetLockPosition = Vector2.Zero;
private Boolean relativeOffsetLock = false;
private Vector2 prevPlayerPosition = Vector2.Zero, prevFacing = Vector2.Zero;
private Vector2 finalTile = Vector2.Zero;
private Vector2 prevTile = Vector2.Zero;
public Boolean isAutoWalking = false;
private Vector2 PlayerFacingVector
{
get
{
switch (Game1.player.FacingDirection)
{
case 0:
return new Vector2(0, -Game1.tileSize);
case 1:
return new Vector2(Game1.tileSize, 0);
case 2:
return new Vector2(0, Game1.tileSize);
case 3:
return new Vector2(-Game1.tileSize, 0);
default:
return Vector2.Zero;
}
}
}
private Vector2 PlayerPosition
{
get
{
int x = Game1.player.GetBoundingBox().Center.X;
int y = Game1.player.GetBoundingBox().Center.Y;
return new Vector2(x, y);
}
}
/// <summary>
/// Return the position of the tile cursor in pixels from the upper-left corner of the map.
/// </summary>
/// <returns>Vector2</returns>
public Vector2 GetTileCursorPosition()
{
Vector2 target = this.PlayerPosition;
if (this.relativeOffsetLock)
{
target += this.relativeOffsetLockPosition;
}
else
{
target += this.PlayerFacingVector + this.ViewingOffset;
}
return target;
}
/// <summary>
/// Return the tile at the position of the tile cursor.
/// </summary>
/// <returns>Vector2</returns>
public Vector2 GetViewingTile()
{
Vector2 position = this.GetTileCursorPosition();
return new Vector2((int)position.X / Game1.tileSize, (int)position.Y / Game1.tileSize);
}
/// <summary>
/// Handle keyboard input related to the tile viewer.
/// </summary>
public void HandleInput()
{
if (MainClass.Config.ToggleRelativeCursorLockKey.JustPressed())
{
this.relativeOffsetLock = !this.relativeOffsetLock;
if (this.relativeOffsetLock)
{
this.relativeOffsetLockPosition = this.PlayerFacingVector + this.ViewingOffset;
}
else
{
this.relativeOffsetLockPosition = Vector2.Zero;
}
MainClass.ScreenReader.Say("Relative cursor lock " + (this.relativeOffsetLock ? "enabled" : "disabled") + ".", true);
}
else if (MainClass.Config.TileCursorPreciseUpKey.JustPressed())
{
this.cursorMoveInput(new Vector2(0, -MainClass.Config.TileCursorPreciseMovementDistance), true);
}
else if (MainClass.Config.TileCursorPreciseRightKey.JustPressed())
{
this.cursorMoveInput(new Vector2(MainClass.Config.TileCursorPreciseMovementDistance, 0), true);
}
else if (MainClass.Config.TileCursorPreciseDownKey.JustPressed())
{
this.cursorMoveInput(new Vector2(0, MainClass.Config.TileCursorPreciseMovementDistance), true);
}
else if (MainClass.Config.TileCursorPreciseLeftKey.JustPressed())
{
this.cursorMoveInput(new Vector2(-MainClass.Config.TileCursorPreciseMovementDistance, 0), true);
}
else if (MainClass.Config.TileCursorUpKey.JustPressed())
{
this.cursorMoveInput(new Vector2(0, -Game1.tileSize));
}
else if (MainClass.Config.TileCursorRightKey.JustPressed())
{
this.cursorMoveInput(new Vector2(Game1.tileSize, 0));
}
else if (MainClass.Config.TileCursorDownKey.JustPressed())
{
this.cursorMoveInput(new Vector2(0, Game1.tileSize));
}
else if (MainClass.Config.TileCursorLeftKey.JustPressed())
{
this.cursorMoveInput(new Vector2(-Game1.tileSize, 0));
}
else if (MainClass.Config.AutoWalkToTileKey.JustPressed() && StardewModdingAPI.Context.IsPlayerFree)
{
this.startAutoWalking();
}
}
private void startAutoWalking()
{
PathFindController controller = new PathFindController(Game1.player, Game1.currentLocation, this.GetViewingTile().ToPoint(), Game1.player.FacingDirection);
controller.allowPlayerPathingInEvent = true;
if (controller.pathToEndPoint != null && controller.pathToEndPoint.Count > 0)
{
Game1.player.controller = controller;
this.isAutoWalking = true;
this.finalTile = this.GetViewingTile();
MainClass.ReadTileFeature.pause();
MainClass.ScreenReader.Say($"Moving to {this.finalTile.X}x {this.finalTile.Y}y", true);
}
else
{
MainClass.ScreenReader.Say($"Cannot move to {this.finalTile.X}x {this.finalTile.Y}y", true);
}
}
/// <summary>
/// Stop the auto walk controller and reset variables
/// </summary>
/// <param name="wasForced">Narrates a message if set to true.</param>
public void stopAutoWalking(bool wasForced = false)
{
this.finalTile = Vector2.Zero;
this.isAutoWalking = false;
Game1.player.controller = null;
MainClass.ReadTileFeature.resume();
if (wasForced)
MainClass.ScreenReader.Say("Stopped moving", true);
}
private void cursorMoveInput(Vector2 delta, Boolean precise = false)
{
if (!tryMoveTileView(delta)) return;
Vector2 position = this.GetTileCursorPosition();
Vector2 tile = this.GetViewingTile();
String? name = TileInfo.GetNameAtTile(tile);
// Prepend the player's name if the viewing tile is occupied by the player itself
if (CurrentPlayer.PositionX == tile.X && CurrentPlayer.PositionY == tile.Y)
{
name = $"{Game1.player.displayName}, {name}";
}
if (name == null)
{
// Report if a tile is empty or blocked if there is nothing on it
if (TileInfo.IsCollidingAtTile(Game1.currentLocation, (int)tile.X, (int)tile.Y))
{
name = "blocked";
}
else
{
name = "empty";
}
}
if (precise)
{
MainClass.ScreenReader.Say($"{name}, {position.X}, {position.Y}", true);
}
else
{
MainClass.ScreenReader.Say($"{name}, {(int)(position.X / Game1.tileSize)}, {(int)(position.Y / Game1.tileSize)}", true);
}
}
private bool tryMoveTileView(Vector2 delta)
{
Vector2 dest = this.GetTileCursorPosition() + delta;
if (!isPositionOnMap(dest)) return false;
if ((MainClass.Config.LimitTileCursorToScreen && Utility.isOnScreen(dest, 0)) || !MainClass.Config.LimitTileCursorToScreen)
{
if (this.relativeOffsetLock)
{
this.relativeOffsetLockPosition += delta;
}
else
{
this.ViewingOffset += delta;
}
return true;
}
return false;
}
private void SnapMouseToPlayer()
{
Vector2 cursorPosition = this.GetTileCursorPosition();
if (allowMouseSnap(cursorPosition))
// Must account for viewport here
Game1.setMousePosition((int)cursorPosition.X - Game1.viewport.X, (int)cursorPosition.Y - Game1.viewport.Y);
}
/// <summary>
/// Handle tile viewer logic.
/// </summary>
public void update()
{
//Reset the viewing cursor to the player when they turn or move. This will not reset the locked offset relative cursor position.
if (this.prevFacing != this.PlayerFacingVector || this.prevPlayerPosition != this.PlayerPosition)
{
this.ViewingOffset = Vector2.Zero;
}
this.prevFacing = this.PlayerFacingVector;
this.prevPlayerPosition = this.PlayerPosition;
if (MainClass.Config.SnapMouse)
this.SnapMouseToPlayer();
if (this.isAutoWalking)
{
if (Vector2.Distance(this.prevTile, CurrentPlayer.Position) >= 2f)
{
prevTile = CurrentPlayer.Position;
Game1.player.checkForFootstep();
}
if (this.finalTile != Vector2.Zero && this.finalTile == CurrentPlayer.Position)
{
MainClass.ScreenReader.Say("Reached destination", true);
this.stopAutoWalking();
}
}
}
private static bool allowMouseSnap(Vector2 point)
{
// Utility.isOnScreen treats a vector as a pixel position, not a tile position
if (!Utility.isOnScreen(point, 0)) return false;
//prevent mousing over the toolbar or any other UI component with the tile cursor
foreach (IClickableMenu menu in Game1.onScreenMenus)
{
//must account for viewport here
if (menu.isWithinBounds((int)point.X - Game1.viewport.X, (int)point.Y - Game1.viewport.Y)) return false;
}
return true;
}
private static bool isPositionOnMap(Vector2 position)
{
var currentLocation = Game1.currentLocation;
// Check whether the position is a warp point, if so then return true, sometimes warp points are 1 tile off the map for example in coops and barns
if (TileInfo.isWarpPointAtTile(currentLocation, (int)(position.X / Game1.tileSize), (int)(position.Y / Game1.tileSize))) return true;
//position does not take viewport into account since the entire map needs to be checked.
Map map = currentLocation.map;
if (position.X < 0 || position.X > map.Layers[0].DisplayWidth) return false;
if (position.Y < 0 || position.Y > map.Layers[0].DisplayHeight) return false;
return true;
}
}
}

View File

@@ -0,0 +1,166 @@
using System.Text.Json;
namespace stardew_access.Features
{
/// <summary>
/// Represents categories that objects can belong to. This class provides predefined categories
/// accessible as static properties and supports adding new categories at runtime. Predefined categories
/// can be accessed like enum values, while both static and dynamic categories can be accessed via the
/// `Categories` property or the `FromString` method.
/// </summary>
/// <remarks>
/// The CATEGORY.Others is used as a default value by the FromString method.
/// Use the FromString method to obtain an existing category.
///
/// Examples:
/// - Access a predefined category like an enum: CATEGORY.Farmers
/// - Obtain a category using the FromString method: CATEGORY.FromString("farmer")
/// - Add a new category: CATEGORY.AddNewCategory("custom_category")
/// - Retrieve a category using the public dictionary: CATEGORY.Categories["custom_category"]
/// - Obtain the string representation of a category: CATEGORY.Farmers.ToString()
/// </remarks>
public sealed class CATEGORY
{
private readonly string _typeKeyWord;
private CATEGORY(string typeKeyWord)
{
_typeKeyWord = typeKeyWord;
}
public override string ToString()
{
return _typeKeyWord;
}
public static IReadOnlyDictionary<string, CATEGORY> Categories => _categories;
private static readonly Dictionary<string, CATEGORY> _categories = new(StringComparer.OrdinalIgnoreCase)
{
{"farmer", new CATEGORY("farmer")},
{"animal", new CATEGORY("animal")},
{"npc", new CATEGORY("npc")},
{"furniture", new CATEGORY("furniture")},
{"flooring", new CATEGORY("flooring")},
{"debris", new CATEGORY("debris")},
{"crop", new CATEGORY("crop")},
{"tree", new CATEGORY("tree")},
{"bush", new CATEGORY("bush")},
{"building", new CATEGORY("building")},
{"mine item", new CATEGORY("mine item")},
{"resource clump", new CATEGORY("resource clump")},
{"container", new CATEGORY("container")},
{"bundle", new CATEGORY("bundle")},
{"door", new CATEGORY("door")},
{"water", new CATEGORY("water")},
{"interactable", new CATEGORY("interactable")},
{"decoration", new CATEGORY("decoration")},
{"machine", new CATEGORY("machine")},
{"bridge", new CATEGORY("bridge")},
{"dropped item", new CATEGORY("dropped item")},
{"other", new CATEGORY("other")}
};
/// <summary>
/// Retrieves a CATEGORY instance by its string name.
/// Names are case-insensitive. If the name is not found, returns the 'Others' category.
/// </summary>
/// <param name="name">The string name of the category to retrieve.</param>
/// <returns>The CATEGORY instance corresponding to the given name or the 'Others' category if not found.</returns>
/// <exception cref="ArgumentNullException">Thrown when the provided name is null.</exception>
public static CATEGORY FromString(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Category name cannot be null or empty.", nameof(name));
}
return Categories.TryGetValue(name, out CATEGORY? category) ? category ?? CATEGORY.Others : CATEGORY.Others;
}
/// <summary>
/// Adds a new CATEGORY with the specified name.
/// Names are case-insensitive.
/// </summary>
/// <param name="name">The name of the new category to be added.</param>
/// <returns>
/// True if a new category was added; false if the category already exists.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown if the provided name is null or empty.</exception>
public static bool AddNewCategory(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name cannot be null or empty.", nameof(name));
}
if (!Categories.ContainsKey(name))
{
_categories[name] = new CATEGORY(name);
return true;
}
return false;
}
public static CATEGORY Farmers => FromString("farmer");
public static CATEGORY FarmAnimals => FromString("animal");
public static CATEGORY NPCs => FromString("npc");
public static CATEGORY Furnitures => FromString("furniture");
public static CATEGORY Flooring => FromString("flooring");
public static CATEGORY Debris => FromString("debris");
public static CATEGORY Crops => FromString("crop");
public static CATEGORY Trees => FromString("tree");
public static CATEGORY Bush => FromString("bush");
public static CATEGORY Buildings => FromString("building");
public static CATEGORY MineItems => FromString("mine item");
public static CATEGORY ResourceClumps => FromString("resource clump");
public static CATEGORY Containers => FromString("container");
public static CATEGORY JunimoBundle => FromString("bundle");
public static CATEGORY Doors => FromString("door");
public static CATEGORY WaterTiles => FromString("water");
public static CATEGORY Interactables => FromString("interactable");
public static CATEGORY Decor => FromString("decoration");
public static CATEGORY Machines => FromString("machine");
public static CATEGORY Bridges => FromString("bridge");
public static CATEGORY DroppedItems => FromString("dropped item");
public static CATEGORY Others => FromString("other");
}
public enum MachineState
{
Ready, Busy, Waiting
}
public static class Utils
{
/// <summary>
/// Loads a JSON file from the specified file name in the assets folder.
/// </summary>
/// <param name="fileName">The name of the JSON file to load.</param>
/// <returns>A <see cref="JsonElement"/> containing the deserialized JSON data, or default if an error occurs.</returns>
public static JsonElement LoadJsonFile(string fileName)
{
string filePath = Path.Combine(MainClass.ModHelper!.DirectoryPath, "assets", fileName);
try
{
string json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<JsonElement>(json);
}
catch (FileNotFoundException ex)
{
MainClass.ErrorLog($"{fileName} file not found: {ex.Message}");
}
catch (JsonException ex)
{
MainClass.ErrorLog($"Error parsing {fileName}: {ex.Message}");
}
catch (Exception ex)
{
MainClass.ErrorLog($"An error occurred while initializing {fileName}: {ex.Message}");
}
return default;
}
}
}

View File

@@ -0,0 +1,90 @@
namespace stardew_access.Features
{
/// <summary>
/// Warns the player when their health or stamina/energy is low. Also warns when its past midnight.
/// </summary>
public class Warnings
{
// Store the previously checked value
private int prevStamina;
private int prevHealth;
private int prevHour;
public Warnings()
{
prevStamina = 100;
prevHealth = 100;
prevHour = 6;
}
public void update()
{
this.checkForHealth();
this.checkForStamina();
this.checkForTimeOfDay();
}
/// <summary>
/// Warns when its past 12:00 am and 1:00 am
/// </summary>
private void checkForTimeOfDay()
{
if (MainClass.ModHelper == null)
return;
int hours = StardewValley.Game1.timeOfDay / 100;
string toSpeak = MainClass.ModHelper.Translation.Get("warnings.time", new { value = CurrentPlayer.TimeOfDay });
if (hours < 1 && prevHour > 2 || hours >= 1 && prevHour < 1)
{
MainClass.ScreenReader.Say(toSpeak, true);
// Pause the read tile feature to prevent interruption in warning message
MainClass.ReadTileFeature.pauseUntil();
}
prevHour = hours;
}
/// <summary>
/// Warns when stamina reaches below 50, 25 and 10.
/// </summary>
public void checkForStamina()
{
if (MainClass.ModHelper == null)
return;
int stamina = CurrentPlayer.PercentStamina;
string toSpeak = MainClass.ModHelper.Translation.Get("warnings.stamina", new { value = stamina });
if ((stamina <= 50 && prevStamina > 50) || (stamina <= 25 && prevStamina > 25) || (stamina <= 10 && prevStamina > 10))
{
MainClass.ScreenReader.Say(toSpeak, true);
// Pause the read tile feature to prevent interruption in warning message
MainClass.ReadTileFeature.pauseUntil();
}
prevStamina = stamina;
}
/// <summary>
/// Warns when health reaches below 50, 25 and 10.
/// </summary>
public void checkForHealth()
{
if (MainClass.ModHelper == null)
return;
int health = CurrentPlayer.PercentHealth;
string toSpeak = MainClass.ModHelper.Translation.Get("warnings.health", new { value = health });
if ((health <= 50 && prevHealth > 50) || (health <= 25 && prevHealth > 25) || (health <= 10 && prevHealth > 10))
{
MainClass.ScreenReader.Say(toSpeak, true);
// Pause the read tile feature to prevent interruption in warning message
MainClass.ReadTileFeature.pauseUntil();
}
prevHealth = health;
}
}
}

View File

@@ -1,9 +1,12 @@
using HarmonyLib; using HarmonyLib;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using stardew_access.Patches; using stardew_access.Patches;
using StardewValley; using StardewValley;
using StardewValley.Characters;
using StardewValley.Menus; using StardewValley.Menus;
using StardewValley.Minigames;
namespace stardew_access namespace stardew_access
{ {
@@ -14,163 +17,216 @@ namespace stardew_access
#region Dialogue Patches #region Dialogue Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(DialogueBox), nameof(DialogueBox.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(DialogueBox), nameof(DialogueBox.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.DialoguePatch)) postfix: new HarmonyMethod(typeof(DialogueBoxPatch), nameof(DialogueBoxPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(DialogueBox), nameof(DialogueBox.receiveLeftClick)), original: AccessTools.Method(typeof(DialogueBox), nameof(DialogueBox.receiveLeftClick)),
postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.ClearDialogueString)) postfix: new HarmonyMethod(typeof(DialogueBoxPatch), nameof(DialogueBoxPatch.RecieveLeftClickPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawHoverText), new Type[] { typeof(SpriteBatch), typeof(string), typeof(SpriteFont), typeof(int), typeof(int), typeof(int), typeof(string), typeof(int), typeof(string[]), typeof(Item), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(float), typeof(CraftingRecipe), typeof(IList<Item>) }), original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawHoverText), new Type[] { typeof(SpriteBatch), typeof(string), typeof(SpriteFont), typeof(int), typeof(int), typeof(int), typeof(string), typeof(int), typeof(string[]), typeof(Item), typeof(int), typeof(int), typeof(int), typeof(int), typeof(int), typeof(float), typeof(CraftingRecipe), typeof(IList<Item>) }),
postfix: new HarmonyMethod(typeof(DialoguePatches), nameof(DialoguePatches.HoverTextPatch)) postfix: new HarmonyMethod(typeof(IClickableMenuPatch), nameof(IClickableMenuPatch.DrawHoverTextPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(NPC), nameof(NPC.drawAboveAlwaysFrontLayer)),
postfix: new HarmonyMethod(typeof(NPCPatch), nameof(NPCPatch.DrawAboveAlwaysFrontLayerPatch))
); );
#endregion #endregion
#region Title Menu Patches #region Title Menu Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(TitleMenu), nameof(TitleMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(TitleMenu), nameof(TitleMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(TitleMenuPatches), nameof(TitleMenuPatches.TitleMenuPatch)) postfix: new HarmonyMethod(typeof(TitleMenuPatch), nameof(TitleMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(LoadGameMenu.SaveFileSlot), nameof(LoadGameMenu.SaveFileSlot.Draw), new Type[] { typeof(SpriteBatch), typeof(int) }), original: AccessTools.Method(typeof(LoadGameMenu.SaveFileSlot), nameof(LoadGameMenu.SaveFileSlot.Draw), new Type[] { typeof(SpriteBatch), typeof(int) }),
postfix: new HarmonyMethod(typeof(TitleMenuPatches), nameof(TitleMenuPatches.LoadGameMenuPatch)) postfix: new HarmonyMethod(typeof(LoadGameMenuPatch), nameof(LoadGameMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(CharacterCustomization), nameof(CharacterCustomization.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(CharacterCustomization), nameof(CharacterCustomization.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(TitleMenuPatches), nameof(TitleMenuPatches.NewGameMenuPatch)) postfix: new HarmonyMethod(typeof(CharacterCustomizationMenuPatch), nameof(CharacterCustomizationMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(CoopMenu), nameof(CoopMenu.update), new Type[] { typeof(GameTime) }), original: AccessTools.Method(typeof(CoopMenu), nameof(CoopMenu.update), new Type[] { typeof(GameTime) }),
postfix: new HarmonyMethod(typeof(TitleMenuPatches), nameof(TitleMenuPatches.CoopMenuPatch)) postfix: new HarmonyMethod(typeof(CoopMenuPatch), nameof(CoopMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(AdvancedGameOptions), nameof(AdvancedGameOptions.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(AdvancedGameOptionsPatch), nameof(AdvancedGameOptionsPatch.DrawPatch))
); );
#endregion #endregion
#region Game Menu Patches #region Game Menu Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(GameMenu), nameof(GameMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(GameMenu), nameof(GameMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.GameMenuPatch)) postfix: new HarmonyMethod(typeof(GameMenuPatch), nameof(GameMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(OptionsPage), nameof(OptionsPage.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(OptionsPage), nameof(OptionsPage.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.OptionsPagePatch)) postfix: new HarmonyMethod(typeof(OptionsPagePatch), nameof(OptionsPagePatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(ExitPage), nameof(ExitPage.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(ExitPage), nameof(ExitPage.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.ExitPagePatch)) postfix: new HarmonyMethod(typeof(ExitPagePatch), nameof(ExitPagePatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(CraftingPage), nameof(CraftingPage.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(CraftingPage), nameof(CraftingPage.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.CraftingPagePatch)) postfix: new HarmonyMethod(typeof(CraftingPagePatch), nameof(CraftingPagePatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(InventoryPage), nameof(InventoryPage.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(InventoryPage), nameof(InventoryPage.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.InventoryPagePatch)) postfix: new HarmonyMethod(typeof(InventoryPagePatch), nameof(InventoryPagePatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(ItemGrabMenu), nameof(ItemGrabMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(ItemGrabMenu), nameof(ItemGrabMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.ItemGrabMenuPatch)) postfix: new HarmonyMethod(typeof(ItemGrabMenuPatch), nameof(ItemGrabMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(GeodeMenu), nameof(GeodeMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(GeodeMenu), nameof(GeodeMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.GeodeMenuPatch)) postfix: new HarmonyMethod(typeof(GeodeMenuPatch), nameof(GeodeMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(ShopMenu), nameof(ShopMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(ShopMenu), nameof(ShopMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.ShopMenuPatch)) postfix: new HarmonyMethod(typeof(ShopMenuPatch), nameof(ShopMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(SocialPage), nameof(SocialPage.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(SocialPage), nameof(SocialPage.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.SocialPagePatch)) postfix: new HarmonyMethod(typeof(SocialPagePatch), nameof(SocialPagePatch.DrawPatch))
); );
harmony.Patch(
original: AccessTools.Method(typeof(CollectionsPage), nameof(CollectionsPage.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(CollectionsPagePatch), nameof(CollectionsPagePatch.DrawPatch))
);
#endregion
#region Bundle Menu Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(JunimoNoteMenu), nameof(JunimoNoteMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(JunimoNoteMenu), nameof(JunimoNoteMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GameMenuPatches), nameof(GameMenuPatches.JunimoNoteMenuPatch)) postfix: new HarmonyMethod(typeof(JunimoNoteMenuPatch), nameof(JunimoNoteMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(JojaCDMenu), nameof(JojaCDMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(JojaCDMenuPatch), nameof(JojaCDMenuPatch.DrawPatch))
); );
#endregion #endregion
#region Menu Patches #region Menu Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(LetterViewerMenu), nameof(LetterViewerMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(LetterViewerMenu), nameof(LetterViewerMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.LetterViewerMenuPatch)) postfix: new HarmonyMethod(typeof(LetterViwerMenuPatch), nameof(LetterViwerMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(ShippingMenu), nameof(ShippingMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(ShippingMenu), nameof(ShippingMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.ShippingMenuPatch)) postfix: new HarmonyMethod(typeof(ShippingMenuPatch), nameof(ShippingMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(LevelUpMenu), nameof(LevelUpMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(LevelUpMenu), nameof(LevelUpMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.LevelUpMenuPatch)) postfix: new HarmonyMethod(typeof(LevelUpMenuPatch), nameof(LevelUpMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(ConfirmationDialog), nameof(ConfirmationDialog.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(ConfirmationDialog), nameof(ConfirmationDialog.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.ConfirmationDialogPatch)) postfix: new HarmonyMethod(typeof(ConfirmationDialogMenuPatch), nameof(ConfirmationDialogMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Constructor(typeof(NamingMenu), new Type[] { typeof(NamingMenu.doneNamingBehavior), typeof(string), typeof(string) }), original: AccessTools.Method(typeof(TitleTextInputMenu), nameof(TitleTextInputMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.NamingMenuPatch)) postfix: new HarmonyMethod(typeof(TitleTextInputMenuPatch), nameof(TitleTextInputMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(NamingMenu), nameof(NamingMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(NamingMenuPatch), nameof(NamingMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(MineElevatorMenu), nameof(MineElevatorMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(MineElevatorMenu), nameof(MineElevatorMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.MineElevatorMenuPatch)) postfix: new HarmonyMethod(typeof(MineElevatorMenuPatch), nameof(MineElevatorMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(LanguageSelectionMenu), nameof(LanguageSelectionMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(LanguageSelectionMenu), nameof(LanguageSelectionMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.LanguageSelectionMenuPatch)) postfix: new HarmonyMethod(typeof(LanguageSelectionMenuPatch), nameof(LanguageSelectionMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(ChooseFromListMenu), nameof(ChooseFromListMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(ChooseFromListMenuPatch), nameof(ChooseFromListMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(TailoringMenu), nameof(TailoringMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(TailoringMenuPatch), nameof(TailoringMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(PondQueryMenu), nameof(PondQueryMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(PondQueryMenuPatch), nameof(PondQueryMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(ForgeMenu), nameof(ForgeMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(ForgeMenuPatch), nameof(ForgeMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(ItemListMenu), nameof(ItemListMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(ItemListMenuPatch), nameof(ItemListMenuPatch.DrawPatch))
); );
#endregion #endregion
#region Quest Patches #region Quest Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(SpecialOrdersBoard), nameof(SpecialOrdersBoard.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(SpecialOrdersBoard), nameof(SpecialOrdersBoard.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(QuestPatches), nameof(QuestPatches.SpecialOrdersBoardPatch)) postfix: new HarmonyMethod(typeof(SpecialOrdersBoardPatch), nameof(SpecialOrdersBoardPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(QuestLog), nameof(QuestLog.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(QuestLog), nameof(QuestLog.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(QuestPatches), nameof(QuestPatches.QuestLogPatch)) postfix: new HarmonyMethod(typeof(QuestLogPatch), nameof(QuestLogPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(Billboard), nameof(Billboard.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(Billboard), nameof(Billboard.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(QuestPatches), nameof(QuestPatches.BillboardPatch)) postfix: new HarmonyMethod(typeof(BillboardPatch), nameof(BillboardPatch.DrawPatch))
); );
#endregion #endregion
#region Chat Menu Patches #region Chat Menu Patches
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(ChatBox), nameof(ChatBox.update), new Type[] { typeof(GameTime) }), original: AccessTools.Method(typeof(ChatBox), nameof(ChatBox.update), new Type[] { typeof(GameTime) }),
postfix: new HarmonyMethod(typeof(ChatManuPatches), nameof(ChatManuPatches.ChatBoxPatch)) postfix: new HarmonyMethod(typeof(ChatBoxPatch), nameof(ChatBoxPatch.UpdatePatch))
); );
#endregion #endregion
#region On Menu CLose Patch #region On Menu CLose Patch
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.exitThisMenu)), original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.exitThisMenu)),
postfix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.IClickableMenuOnExitPatch)) postfix: new HarmonyMethod(typeof(IClickableMenuPatch), nameof(IClickableMenuPatch.ExitThisMenuPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(Game1), nameof(Game1.exitActiveMenu)), original: AccessTools.Method(typeof(Game1), nameof(Game1.exitActiveMenu)),
prefix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.Game1ExitActiveMenuPatch)) prefix: new HarmonyMethod(typeof(Game1Patch), nameof(Game1Patch.ExitActiveMenuPatch))
); );
#endregion #endregion
@@ -178,24 +234,83 @@ namespace stardew_access
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(CarpenterMenu), nameof(CarpenterMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(CarpenterMenu), nameof(CarpenterMenu.draw), new Type[] { typeof(SpriteBatch) }),
prefix: new HarmonyMethod(typeof(BuildingNAnimalMenuPatches), nameof(BuildingNAnimalMenuPatches.CarpenterMenuPatch)) prefix: new HarmonyMethod(typeof(CarpenterMenuPatch), nameof(CarpenterMenuPatch.DrawPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(PurchaseAnimalsMenu), nameof(PurchaseAnimalsMenu.draw), new Type[] { typeof(SpriteBatch) }), original: AccessTools.Method(typeof(PurchaseAnimalsMenu), nameof(PurchaseAnimalsMenu.draw), new Type[] { typeof(SpriteBatch) }),
prefix: new HarmonyMethod(typeof(BuildingNAnimalMenuPatches), nameof(BuildingNAnimalMenuPatches.PurchaseAnimalsMenuPatch)) prefix: new HarmonyMethod(typeof(PurchaseAnimalsMenuPatch), nameof(PurchaseAnimalsMenuPatch.DrawPatch))
); );
harmony.Patch(
original: AccessTools.Method(typeof(AnimalQueryMenu), nameof(AnimalQueryMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(AnimalQueryMenuPatch), nameof(AnimalQueryMenuPatch.DrawPatch))
);
#endregion
#region Donation Menus
harmony.Patch(
original: AccessTools.Method(typeof(MuseumMenu), nameof(MuseumMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(MuseumMenuPatch), nameof(MuseumMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(MuseumMenu), nameof(MuseumMenu.receiveKeyPress), new Type[] { typeof(Keys) }),
prefix: new HarmonyMethod(typeof(MuseumMenuPatch), nameof(MuseumMenuPatch.RecieveKeyPressPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(FieldOfficeMenu), nameof(FieldOfficeMenu.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(FieldOfficeMenuPatch), nameof(FieldOfficeMenuPatch.DrawPatch))
);
#endregion
#region Mini Games
harmony.Patch(
original: AccessTools.Method(typeof(Intro), nameof(Intro.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(IntroPatch), nameof(IntroPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(GrandpaStory), nameof(GrandpaStory.draw), new Type[] { typeof(SpriteBatch) }),
postfix: new HarmonyMethod(typeof(GrandpaStoryPatch), nameof(GrandpaStoryPatch.DrawPatch))
);
#endregion #endregion
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(Game1), nameof(Game1.playSound)), original: AccessTools.Method(typeof(Game1), nameof(Game1.playSound)),
prefix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.PlaySoundPatch)) prefix: new HarmonyMethod(typeof(Game1Patch), nameof(Game1Patch.PlaySoundPatch))
); );
harmony.Patch( harmony.Patch(
original: AccessTools.Method(typeof(InstanceGame), nameof(InstanceGame.Exit)), original: AccessTools.Method(typeof(InstanceGame), nameof(InstanceGame.Exit)),
prefix: new HarmonyMethod(typeof(MenuPatches), nameof(MenuPatches.ExitEventPatch)) prefix: new HarmonyMethod(typeof(InstanceGamePatch), nameof(InstanceGamePatch.ExitPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(TextBox), nameof(TextBox.Draw)),
prefix: new HarmonyMethod(typeof(TextBoxPatch), nameof(TextBoxPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(TextEntryMenu), nameof(TextEntryMenu.draw), new Type[] { typeof(SpriteBatch) }),
prefix: new HarmonyMethod(typeof(TextEntryMenuPatch), nameof(TextEntryMenuPatch.DrawPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(TextEntryMenu), nameof(TextEntryMenu.Close)),
prefix: new HarmonyMethod(typeof(TextEntryMenuPatch), nameof(TextEntryMenuPatch.ClosePatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(Game1), nameof(Game1.closeTextEntry)),
prefix: new HarmonyMethod(typeof(Game1Patch), nameof(Game1Patch.CloseTextEntryPatch))
);
harmony.Patch(
original: AccessTools.Method(typeof(TrashBear), nameof(TrashBear.checkAction)),
postfix: new HarmonyMethod(typeof(TrashBearPatch), nameof(TrashBearPatch.CheckActionPatch))
); );
} }
} }

View File

@@ -0,0 +1,95 @@
using StardewModdingAPI.Utilities;
namespace stardew_access
{
internal class ModConfig
{
// https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#SButton button key codes
#region Simulate mouse clicks
public KeybindList LeftClickMainKey { get; set; } = KeybindList.Parse("LeftControl + Enter"); // Primary key to simulate mouse left click
public KeybindList RightClickMainKey { get; set; } = KeybindList.Parse("LeftShift + Enter"); // Primary key to simulate mouse right click
public KeybindList LeftClickAlternateKey { get; set; } = KeybindList.Parse("OemOpenBrackets"); // Secondary key to simulate mouse left click
public KeybindList RightClickAlternateKey { get; set; } = KeybindList.Parse("OemCloseBrackets"); // Secondary key to simulate mouse right click
#endregion
#region Chat menu
public KeybindList ChatMenuNextKey { get; set; } = KeybindList.Parse("PageUp"); // Read previous chat message
public KeybindList ChatMenuPreviousKey { get; set; } = KeybindList.Parse("PageDown"); // Read next chat message
#endregion
#region Read tile
public Boolean ReadTile { get; set; } = true; // Toggle this feature.
public KeybindList ReadTileKey { get; set; } = KeybindList.Parse("J"); // Manually trigger read tile for the tile player is *looking at*.
public KeybindList ReadStandingTileKey { get; set; } = KeybindList.Parse("LeftAlt + J"); // Manually trigger read tile for the tile player is *standing on*.
public Boolean ReadFlooring { get; set; } = false; // Toggle reading floorings.
public Boolean WateredToggle { get; set; } = true; // Toggle speaking watered or unwatered for crops.
#endregion
#region Tile viewer
public KeybindList TileCursorUpKey { get; set; } = KeybindList.Parse("Up"); // Move the cursor one tile up
public KeybindList TileCursorRightKey { get; set; } = KeybindList.Parse("Right"); // Move the cursor one tile right
public KeybindList TileCursorDownKey { get; set; } = KeybindList.Parse("Down"); // Move the cursor one tile down
public KeybindList TileCursorLeftKey { get; set; } = KeybindList.Parse("Left"); // Move the cursor one tile left
public KeybindList TileCursorPreciseUpKey { get; set; } = KeybindList.Parse("LeftShift + Up"); // Move the cursor up by precision i.e. pixel by pixel
public KeybindList TileCursorPreciseRightKey { get; set; } = KeybindList.Parse("LeftShift + Right"); // Move the cursor right by precision i.e. pixel by pixel
public KeybindList TileCursorPreciseDownKey { get; set; } = KeybindList.Parse("LeftShift + Down"); // Move the cursor down by precision i.e. pixel by pixel
public KeybindList TileCursorPreciseLeftKey { get; set; } = KeybindList.Parse("LeftShift + Left"); // Move the cursor left by precision i.e. pixel by pixel
public KeybindList ToggleRelativeCursorLockKey { get; set; } = KeybindList.Parse("L"); // Toggles realative cursor lock i.e. if enabled, the cursor will reset when player moves.
public KeybindList AutoWalkToTileKey { get; set; } = KeybindList.Parse("LeftControl + Enter"); // Auto walk to the tile
public bool LimitTileCursorToScreen { get; set; } = false; // #TODO add command for this // Toggle whether to prevent cursor from going out of screen.
public int TileCursorPreciseMovementDistance { get; set; } = 8; // Specifies the number of pixels the cursor should move when using precision movement i.e. with *left shift*.
#endregion
#region Radar
public Boolean Radar { get; set; } = false;
public Boolean RadarStereoSound { get; set; } = true;
#endregion
#region Menu Keys
public KeybindList PrimaryInfoKey { get; set; } = KeybindList.Parse("C");
// Character Creation menu (new game menu) keys
public KeybindList CharacterCreationMenuNextKey { get; set; } = KeybindList.Parse("Right");
public KeybindList CharacterCreationMenuPreviousKey { get; set; } = KeybindList.Parse("Left");
public KeybindList CharacterCreationMenuSliderIncreaseKey { get; set; } = KeybindList.Parse("Up");
public KeybindList CharacterCreationMenuSliderLargeIncreaseKey { get; set; } = KeybindList.Parse("PageUp");
public KeybindList CharacterCreationMenuSliderDecreaseKey { get; set; } = KeybindList.Parse("Down");
public KeybindList CharacterCreationMenuSliderLargeDecreaseKey { get; set; } = KeybindList.Parse("PageDown");
public KeybindList CharacterCreationMenuDesignToggleKey { get; set; } = KeybindList.Parse("LeftControl + Space");
// Bundle menu keys
public KeybindList BundleMenuIngredientsKey { get; set; } = KeybindList.Parse("I");
public KeybindList BundleMenuInventoryItemsKey { get; set; } = KeybindList.Parse("C");
public KeybindList BundleMenuPurchaseButtonKey { get; set; } = KeybindList.Parse("P");
public KeybindList BundleMenuIngredientsInputSlotKey { get; set; } = KeybindList.Parse("V");
public KeybindList BundleMenuBackButtonKey { get; set; } = KeybindList.Parse("Back");
// Menus with secondary inventory(shop inventory or chest inventory or crafting recipe list)
public KeybindList SnapToFirstInventorySlotKey { get; set; } = KeybindList.Parse("I");
public KeybindList SnapToFirstSecondaryInventorySlotKey { get; set; } = KeybindList.Parse("LeftShift + I");
// Crafting menu
public KeybindList CraftingMenuCycleThroughRecipiesKey { get; set; } = KeybindList.Parse("C");
#endregion
#region Others
public KeybindList HealthNStaminaKey { get; set; } = KeybindList.Parse("H"); // Narrate health and stamina.
public bool HealthNStaminaInPercentage { get; set; } = true;
public KeybindList PositionKey { get; set; } = KeybindList.Parse("K"); // Narrate player position.
public KeybindList LocationKey { get; set; } = KeybindList.Parse("LeftAlt + K"); // Narrate current location name.
public KeybindList MoneyKey { get; set; } = KeybindList.Parse("R"); // Narrate the money the player has currently.
public KeybindList TimeNSeasonKey { get; set; } = KeybindList.Parse("Q"); // Narrate the time of day, day and date and season
public Boolean VerboseCoordinates { get; set; } = true;
public Boolean SnapMouse { get; set; } = true; // Toggles the snap mouse feature
public Boolean Warning { get; set; } = true; // Toggles the warnings feature
public Boolean TTS { get; set; } = true; // Toggles the screen reader/tts.
public Boolean TrackDroppedItems {get; set;} = true; // Toggles detecting the dropped items.
public Single SpeechRate {get; set;} = 220; // Sets speech rate for the Mac TTS.
public Boolean DisableInventoryVerbosity {get; set;} = false; // If enabled, does not speaks 'not usable here' and 'donatable' in inventories
#endregion
// TODO Add the exclusion and focus list too
// public String ExclusionList { get; set; } = "test";
}
}

View File

@@ -11,44 +11,84 @@ namespace stardew_access
{ {
public class MainClass : Mod public class MainClass : Mod
{ {
#region Global Vars & Properties
#pragma warning disable CS8603
private static int prevDate = -99;
private static ModConfig? config;
private Harmony? harmony; private Harmony? harmony;
public static bool readTile = true; private static IMonitor? monitor;
public static bool snapMouse = true; private static Radar? radarFeature;
public static bool isNarratingHudMessage = false;
public static bool radar = false;
public static bool radarDebug = false;
public static bool radarStereoSound = true;
private static IMonitor monitor;
public static string hudMessageQueryKey = "";
private static Radar radarFeature;
private static IScreenReader? screenReader; private static IScreenReader? screenReader;
private static IModHelper modHelper; private static IModHelper? modHelper;
private static TileViewer? tileViewer;
private static Warnings? warnings;
private static ReadTile? readTile;
public static IModHelper ModHelper { get => modHelper; } internal static ModConfig Config { get => config; set => config = value; }
public static Radar RadarFeature { get => radarFeature; set => radarFeature = value; } public static IModHelper? ModHelper { get => modHelper; }
public static IScreenReader GetScreenReader() public static Radar RadarFeature
{ {
if (screenReader == null) get
screenReader = new ScreenReaderController().Initialize(); {
return screenReader; if (radarFeature == null)
radarFeature = new Radar();
return radarFeature;
}
set => radarFeature = value;
} }
public static void SetScreenReader(IScreenReader value) public static string hudMessageQueryKey = "";
public static bool isNarratingHudMessage = false;
public static bool radarDebug = false;
public static IScreenReader ScreenReader
{ {
screenReader = value; get
{
if (screenReader == null)
screenReader = new ScreenReaderController().Initialize();
return screenReader;
}
set => screenReader = value;
} }
public static IMonitor GetMonitor() public static TileViewer TileViewerFeature
{ {
return monitor; get
{
if (tileViewer == null)
tileViewer = new TileViewer();
return tileViewer;
}
} }
public static void SetMonitor(IMonitor value) public static ReadTile ReadTileFeature
{ {
monitor = value; get
{
if (readTile == null)
readTile = new ReadTile();
return readTile;
}
} }
public static Warnings WarningsFeature
{
get
{
if (warnings == null)
warnings = new Warnings();
return warnings;
}
}
#endregion
/********* /*********
** Public methods ** Public methods
*********/ *********/
@@ -57,46 +97,51 @@ namespace stardew_access
public override void Entry(IModHelper helper) public override void Entry(IModHelper helper)
{ {
#region Initializations #region Initializations
Config = helper.ReadConfig<ModConfig>();
SetMonitor(base.Monitor); // Inititalize monitor monitor = base.Monitor; // Inititalize monitor
modHelper = helper; modHelper = helper;
Game1.options.setGamepadMode("force_on"); Game1.options.setGamepadMode("force_on");
SetScreenReader(new ScreenReaderController().Initialize()); ScreenReader = new ScreenReaderController().Initialize();
ScreenReader.Say("Initializing Stardew Access", true);
CustomSoundEffects.Initialize(); CustomSoundEffects.Initialize();
CustomCommands.Initialize(); CustomCommands.Initialize();
RadarFeature = new Radar();
harmony = new Harmony(ModManifest.UniqueID); harmony = new Harmony(ModManifest.UniqueID);
HarmonyPatches.Initialize(harmony); HarmonyPatches.Initialize(harmony);
//Initialize marked locations //Initialize marked locations
for (int i = 0; i < BuildingNAnimalMenuPatches.marked.Length; i++) for (int i = 0; i < BuildingOperations.marked.Length; i++)
{ {
BuildingNAnimalMenuPatches.marked[i] = Vector2.Zero; BuildingOperations.marked[i] = Vector2.Zero;
} }
for (int i = 0; i < BuildingNAnimalMenuPatches.availableBuildings.Length; i++) for (int i = 0; i < BuildingOperations.availableBuildings.Length; i++)
{ {
BuildingNAnimalMenuPatches.availableBuildings[i] = null; BuildingOperations.availableBuildings[i] = null;
} }
#endregion #endregion
helper.Events.Input.ButtonPressed += this.OnButtonPressed; helper.Events.Input.ButtonPressed += this.OnButtonPressed;
helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked; helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked;
helper.Events.GameLoop.DayStarted += this.onDayStarted;
helper.Events.Display.MenuChanged += this.onMenuChanged;
AppDomain.CurrentDomain.DomainUnload += OnExit; AppDomain.CurrentDomain.DomainUnload += OnExit;
AppDomain.CurrentDomain.ProcessExit += OnExit; AppDomain.CurrentDomain.ProcessExit += OnExit;
} }
public void OnExit(object? sender, EventArgs? e) private void onMenuChanged(object? sender, MenuChangedEventArgs e)
{ {
// Don't if this ever gets called or not but, just in case if it does. TextBoxPatch.activeTextBoxes = "";
if (GetScreenReader() != null) if (e.OldMenu != null)
GetScreenReader().CloseScreenReader(); {
MainClass.DebugLog($"Switched from {e.OldMenu.GetType().ToString()} menu, performing cleanup...");
IClickableMenuPatch.Cleanup(e.OldMenu);
}
} }
/// <summary>Returns the Screen Reader class for other mods to use.</summary> /// <summary>Returns the Screen Reader class for other mods to use.</summary>
@@ -105,31 +150,77 @@ namespace stardew_access
return new API(); return new API();
} }
public void OnExit(object? sender, EventArgs? e)
{
// This closes the connection with the screen reader, important for linux
// Don't know if this ever gets called or not but, just in case if it does.
if (ScreenReader != null)
ScreenReader.CloseScreenReader();
}
private void onDayStarted(object? sender, DayStartedEventArgs? e)
{
StaticTiles.LoadTilesFiles();
StaticTiles.SetupTilesDicts();
}
private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e) private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e)
{ {
if (!Context.IsPlayerFree) if (!Context.IsPlayerFree)
return; return;
// Reset variables // Narrates currently selected inventory slot
MenuPatches.resetGlobalVars();
QuestPatches.resetGlobalVars();
Other.narrateCurrentSlot(); Other.narrateCurrentSlot();
// Narrate current location's name
Other.narrateCurrentLocation(); Other.narrateCurrentLocation();
//handle TileCursor update logic
TileViewerFeature.update();
if (snapMouse) if (Config.Warning)
Other.SnapMouseToPlayer(); WarningsFeature.update();
if (!ReadTile.isReadingTile && readTile) if (Config.ReadTile)
ReadTile.run(); ReadTileFeature.update();
if (!RadarFeature.isRunning && radar) RunRadarFeatureIfEnabled();
RadarFeature.Run();
if (!isNarratingHudMessage) RunHudMessageNarration();
RefreshBuildListIfRequired();
async void RunRadarFeatureIfEnabled()
{ {
Other.narrateHudMessages(); if (!RadarFeature.isRunning && Config.Radar)
{
RadarFeature.isRunning = true;
RadarFeature.Run();
await Task.Delay(RadarFeature.delay);
RadarFeature.isRunning = false;
}
}
async void RunHudMessageNarration()
{
if (!isNarratingHudMessage)
{
isNarratingHudMessage = true;
Other.narrateHudMessages();
await Task.Delay(300);
isNarratingHudMessage = false;
}
}
void RefreshBuildListIfRequired()
{
if (Game1.player != null)
{
if (Game1.timeOfDay >= 600 && prevDate != CurrentPlayer.Date)
{
prevDate = CurrentPlayer.Date;
DebugLog("Refreshing buildlist...");
CustomCommands.onBuildListCalled();
}
}
} }
} }
@@ -138,74 +229,158 @@ namespace stardew_access
if (e == null) if (e == null)
return; return;
if (Game1.activeClickableMenu != null) void SimulateMouseClicks(Action<int, int> leftClickHandler, Action<int, int> rightClickHandler)
{ {
bool isLeftShiftPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift); int mouseX = Game1.getMouseX(true);
bool isLeftControlPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftControl); int mouseY = Game1.getMouseY(true);
// Perform Left Click if (Config.LeftClickMainKey.JustPressed() || Config.LeftClickAlternateKey.JustPressed())
if (Equals(e.Button, SButton.OemOpenBrackets))
{ {
Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true)); leftClickHandler(mouseX, mouseY);
} }
if (isLeftControlPressed && Equals(e.Button, SButton.Enter)) else if (Config.RightClickMainKey.JustPressed() || Config.RightClickAlternateKey.JustPressed())
{ {
Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true)); rightClickHandler(mouseX, mouseY);
}
// Perform Right CLick
if (Equals(e.Button, SButton.OemCloseBrackets))
{
Game1.activeClickableMenu.receiveRightClick(Game1.getMouseX(true), Game1.getMouseY(true));
}
if (isLeftShiftPressed && Equals(e.Button, SButton.Enter))
{
Game1.activeClickableMenu.receiveRightClick(Game1.getMouseX(true), Game1.getMouseY(true));
} }
} }
#region Simulate left and right clicks
if (!TextBoxPatch.isAnyTextBoxActive)
{
if (Game1.activeClickableMenu != null)
{
SimulateMouseClicks(
(x, y) => Game1.activeClickableMenu.receiveLeftClick(x, y),
(x, y) => Game1.activeClickableMenu.receiveRightClick(x, y)
);
}
else if (Game1.currentMinigame != null)
{
SimulateMouseClicks(
(x, y) => Game1.currentMinigame.receiveLeftClick(x, y),
(x, y) => Game1.currentMinigame.receiveRightClick(x, y)
);
}
}
#endregion
if (!Context.IsPlayerFree) if (!Context.IsPlayerFree)
return; return;
// Narrate health and stamina void Narrate(string message) => MainClass.ScreenReader.Say(message, true);
if (Equals(e.Button, SButton.H))
bool IsMovementKey(SButton button)
{ {
string toSpeak = $"Health is {CurrentPlayer.getHealth()} and Stamina is {CurrentPlayer.getStamina()}"; return button.Equals(SButtonExtensions.ToSButton(Game1.options.moveUpButton[0]))
MainClass.GetScreenReader().Say(toSpeak, true); || button.Equals(SButtonExtensions.ToSButton(Game1.options.moveDownButton[0]))
|| button.Equals(SButtonExtensions.ToSButton(Game1.options.moveLeftButton[0]))
|| button.Equals(SButtonExtensions.ToSButton(Game1.options.moveRightButton[0]));
}
// Stops the auto walk controller if any movement key(WASD) is pressed
if (TileViewerFeature.isAutoWalking && IsMovementKey(e.Button))
{
TileViewerFeature.stopAutoWalking(wasForced: true);
}
// Narrate Current Location
if (Config.LocationKey.JustPressed())
{
Narrate(Game1.currentLocation.Name);
return;
} }
// Narrate Position // Narrate Position
if (Equals(e.Button, SButton.K)) if (Config.PositionKey.JustPressed())
{ {
string toSpeak = $"X: {CurrentPlayer.getPositionX()} , Y: {CurrentPlayer.getPositionY()}"; string toSpeak = Config.VerboseCoordinates
MainClass.GetScreenReader().Say(toSpeak, true); ? $"X: {CurrentPlayer.PositionX}, Y: {CurrentPlayer.PositionY}"
: $"{CurrentPlayer.PositionX}, {CurrentPlayer.PositionY}";
Narrate(toSpeak);
return;
}
// Narrate health and stamina
if (Config.HealthNStaminaKey.JustPressed())
{
if (ModHelper == null)
return;
string toSpeak = Config.HealthNStaminaInPercentage
? ModHelper.Translation.Get("manuallytriggered.healthnstamina.percent", new { health = CurrentPlayer.PercentHealth, stamina = CurrentPlayer.PercentStamina })
: ModHelper.Translation.Get("manuallytriggered.healthnstamina.normal", new { health = CurrentPlayer.CurrentHealth, stamina = CurrentPlayer.CurrentStamina });
Narrate(toSpeak);
return;
} }
// Narrate money at hand // Narrate money at hand
if (Equals(e.Button, SButton.R)) if (Config.MoneyKey.JustPressed())
{ {
string toSpeak = $"You have {CurrentPlayer.getMoney()}g"; Narrate($"You have {CurrentPlayer.Money}g");
MainClass.GetScreenReader().Say(toSpeak, true); return;
} }
// Narrate time and season // Narrate time and season
if (Equals(e.Button, SButton.Q)) if (Config.TimeNSeasonKey.JustPressed())
{ {
string toSpeak = $"Time is {CurrentPlayer.getTimeOfDay()} and it is {CurrentPlayer.getDay()} {CurrentPlayer.getDate()} of {CurrentPlayer.getSeason()}"; Narrate($"Time is {CurrentPlayer.TimeOfDay} and it is {CurrentPlayer.Day} {CurrentPlayer.Date} of {CurrentPlayer.Season}");
MainClass.GetScreenReader().Say(toSpeak, true); return;
} }
// Manual read tile // Manual read tile at player's position
if (Equals(e.Button, SButton.J)) if (Config.ReadStandingTileKey.JustPressed())
{ {
ReadTile.run(manuallyTriggered: true); ReadTileFeature.run(manuallyTriggered: true, playersPosition: true);
return;
} }
/*if (Equals(e.Button, SButton.B)) // Manual read tile at looking tile
if (Config.ReadTileKey.JustPressed())
{ {
Game1.player.controller = new PathFindController(Game1.player, Game1.currentLocation, new Point(49,13), 2); ReadTileFeature.run(manuallyTriggered: true);
monitor.Log($"{Game1.player.controller.pathToEndPoint==null}", LogLevel.Debug); // true if path not found return;
}*/ }
// Tile viewing cursor keys
TileViewerFeature.HandleInput();
}
public static string Translate(string translationKey)
{
if (ModHelper == null) return "null";
return ModHelper.Translation.Get(translationKey);
}
public static string Translate(string translationKey, object? tokens)
{
if (ModHelper == null) return "null";
return ModHelper.Translation.Get(translationKey, tokens);
}
private static void LogMessage(string message, LogLevel logLevel)
{
if (monitor == null)
return;
monitor.Log(message, logLevel);
}
public static void ErrorLog(string message)
{
LogMessage(message, LogLevel.Error);
}
public static void InfoLog(string message)
{
LogMessage(message, LogLevel.Info);
}
public static void DebugLog(string message)
{
LogMessage(message, LogLevel.Debug);
} }
} }
} }

View File

@@ -1,574 +0,0 @@
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.Menus;
using StardewValley.Objects;
namespace stardew_access.Patches
{
internal class BuildingNAnimalMenuPatches
{
internal static Vector2[] marked = new Vector2[10];
internal static Building?[] availableBuildings = new Building[100];
internal static CarpenterMenu? carpenterMenu = null;
internal static string carpenterMenuQuery = "", purchaseAnimalMenuQuery = "";
internal static bool isSayingBlueprintInfo = false;
internal static string prevBlueprintInfo = "";
internal static bool isOnFarm = false, isUpgrading = false, isDemolishing = false, isPainting = false, isConstructing = false, isMoving = false, isMagicalConstruction = false;
internal static bool firstTimeInNamingMenu = true;
internal static PurchaseAnimalsMenu? purchaseAnimalsMenu;
internal static void PurchaseAnimalsMenuPatch(PurchaseAnimalsMenu __instance, bool ___onFarm, bool ___namingAnimal, TextBox ___textBox)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY(); // Mouse x and y position
purchaseAnimalsMenu = __instance;
isOnFarm = ___onFarm;
if (___onFarm && ___namingAnimal)
{
string toSpeak = "";
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "Cancel Button";
}
else if (__instance.doneNamingButton != null && __instance.doneNamingButton.containsPoint(x, y))
{
toSpeak = "OK Button";
}
else if (__instance.randomButton != null && __instance.randomButton.containsPoint(x, y))
{
toSpeak = "Random Name Button";
}
else if (__instance.textBoxCC != null && __instance.textBoxCC.containsPoint(x, y))
{
toSpeak = "Name Text Box";
string? value = ___textBox.Text;
if (value != "" && value != null && value != "null")
toSpeak = $"{toSpeak}, Value: {value}";
}
if (purchaseAnimalMenuQuery != toSpeak)
{
purchaseAnimalMenuQuery = toSpeak;
if (firstTimeInNamingMenu)
{
toSpeak = $"Enter the name of animal in the name text box. {toSpeak}";
firstTimeInNamingMenu = false;
}
MainClass.GetScreenReader().Say(toSpeak, true);
}
}
else if (___onFarm && !___namingAnimal)
{
firstTimeInNamingMenu = true;
}
else if (!___onFarm && !___namingAnimal)
{
firstTimeInNamingMenu = true;
if (__instance.hovered != null)
{
string toSpeak = "";
if (((StardewValley.Object)__instance.hovered.item).Type != null)
{
toSpeak = ((StardewValley.Object)__instance.hovered.item).Type;
}
else
{
string displayName = PurchaseAnimalsMenu.getAnimalTitle(__instance.hovered.hoverText);
int price = __instance.hovered.item.salePrice();
string description = PurchaseAnimalsMenu.getAnimalDescription(__instance.hovered.hoverText);
toSpeak = $"{displayName}, Price: {price}g, Description: {description}";
}
if (purchaseAnimalMenuQuery != toSpeak)
{
purchaseAnimalMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void CarpenterMenuPatch(
CarpenterMenu __instance, bool ___onFarm, List<Item> ___ingredients, int ___price,
List<BluePrint> ___blueprints, int ___currentBlueprintIndex, bool ___upgrading, bool ___demolishing, bool ___moving,
bool ___painting, bool ___magicalConstruction)
{
try
{
isOnFarm = ___onFarm;
carpenterMenu = __instance;
isMagicalConstruction = ___magicalConstruction;
if (!___onFarm)
{
isUpgrading = false;
isDemolishing = false;
isPainting = false;
isMoving = false;
isConstructing = false;
#region The blueprint menu
BluePrint currentBluprint = __instance.CurrentBlueprint;
if (currentBluprint == null)
return;
int x = Game1.getMouseX(), y = Game1.getMouseY(); // Mouse x and y position
bool isBPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.B);
string ingredients = "";
string name = currentBluprint.displayName;
string upgradeName = currentBluprint.nameOfBuildingToUpgrade;
string description = currentBluprint.description;
string price = $"{___price}g";
string blueprintInfo;
int width = currentBluprint.tilesWidth;
int height = currentBluprint.tilesHeight;
#region Get ingredients
for (int i = 0; i < ___ingredients.Count; i++)
{
string itemName = ___ingredients[i].DisplayName;
int itemStack = ___ingredients[i].Stack;
string itemQuality = "";
int qualityValue = ((StardewValley.Object)___ingredients[i]).quality;
if (qualityValue == 1)
{
itemQuality = "Silver quality";
}
else if (qualityValue == 2 || qualityValue == 3)
{
itemQuality = "Gold quality";
}
else if (qualityValue >= 4)
{
itemQuality = "Iridium quality";
}
ingredients = $"{ingredients}, {itemStack} {itemName} {itemQuality}";
}
#endregion
blueprintInfo = $"{name}, Price: {price}, Ingredients: {ingredients}, Dimensions: {width} width and {height} height, Description: {description}";
if (isBPressed && !isSayingBlueprintInfo)
{
SayBlueprintInfo(blueprintInfo);
}
else if (prevBlueprintInfo != blueprintInfo)
{
prevBlueprintInfo = blueprintInfo;
SayBlueprintInfo(blueprintInfo);
}
else
{
if (__instance.backButton != null && __instance.backButton.containsPoint(x, y))
{
string toSpeak = "Previous Blueprint";
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
if (__instance.forwardButton != null && __instance.forwardButton.containsPoint(x, y))
{
string toSpeak = "Next Blueprint";
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
if (__instance.demolishButton != null && __instance.demolishButton.containsPoint(x, y))
{
string toSpeak = $"Demolish Building" + (__instance.CanDemolishThis(___blueprints[___currentBlueprintIndex]) ? "" : ", cannot demolish building");
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
string toSpeak = "Construct Building" + (___blueprints[___currentBlueprintIndex].doesFarmerHaveEnoughResourcesToBuild() ? "" : ", cannot cunstrut building, not enough resources to build.");
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
if (__instance.moveButton != null && __instance.moveButton.containsPoint(x, y))
{
string toSpeak = "Move Building";
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
if (__instance.paintButton != null && __instance.paintButton.containsPoint(x, y))
{
string toSpeak = "Paint Building";
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
if (__instance.cancelButton != null && __instance.cancelButton.containsPoint(x, y))
{
string toSpeak = "Cancel Button";
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
return;
}
}
#endregion
}
else
{
if (___demolishing)
isDemolishing = true;
else if (___upgrading)
isUpgrading = true;
else if (___painting)
isPainting = true;
else if (___moving)
isMoving = true;
else
isConstructing = true;
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
private static async void SayBlueprintInfo(string info)
{
isSayingBlueprintInfo = true;
MainClass.GetScreenReader().Say(info, true);
await Task.Delay(300);
isSayingBlueprintInfo = false;
}
public static string? Demolish(Building? toDemolish)
{
if (toDemolish == null)
return null;
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::654)
Farm farm = (Farm)Game1.getLocationFromName("Farm");
Action buildingLockFailed = delegate
{
if (isDemolishing)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_LockFailed");
}
};
Action continueDemolish = delegate
{
if (isDemolishing && toDemolish != null && farm.buildings.Contains(toDemolish))
{
if ((int)toDemolish.daysOfConstructionLeft > 0 || (int)toDemolish.daysUntilUpgrade > 0)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_DuringConstruction");
}
else if (toDemolish.indoors.Value != null && toDemolish.indoors.Value is AnimalHouse && ((AnimalHouse)toDemolish.indoors.Value).animalsThatLiveHere.Count > 0)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_AnimalsHere");
}
else if (toDemolish.indoors.Value != null && toDemolish.indoors.Value.farmers.Any())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_PlayerHere");
}
else
{
if (toDemolish.indoors.Value != null && toDemolish.indoors.Value is Cabin)
{
foreach (Farmer current in Game1.getAllFarmers())
{
if (current.currentLocation != null && current.currentLocation.Name == ((Cabin)toDemolish.indoors.Value).GetCellarName())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_PlayerHere");
return;
}
}
}
if (toDemolish.indoors.Value is Cabin && ((Cabin)toDemolish.indoors.Value).farmhand.Value.isActive())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_FarmhandOnline");
}
else
{
toDemolish.BeforeDemolish();
Chest? chest = null;
if (toDemolish.indoors.Value is Cabin)
{
List<Item> list = ((Cabin)toDemolish.indoors.Value).demolish();
if (list.Count > 0)
{
chest = new Chest(playerChest: true);
chest.fixLidFrame();
chest.items.Set(list);
}
}
if (farm.destroyStructure(toDemolish))
{
_ = (int)toDemolish.tileY;
_ = (int)toDemolish.tilesHigh;
Game1.flashAlpha = 1f;
toDemolish.showDestroyedAnimation(Game1.getFarm());
Game1.playSound("explosion");
Utility.spreadAnimalsAround(toDemolish, farm);
if (carpenterMenu != null)
DelayedAction.functionAfterDelay(carpenterMenu.returnToCarpentryMenu, 1500);
// freeze = true;
if (chest != null)
{
farm.objects[new Vector2((int)toDemolish.tileX + (int)toDemolish.tilesWide / 2, (int)toDemolish.tileY + (int)toDemolish.tilesHigh / 2)] = chest;
}
}
}
}
}
};
if (toDemolish != null)
{
if (toDemolish.indoors.Value != null && toDemolish.indoors.Value is Cabin && !Game1.IsMasterGame)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantDemolish_LockFailed");
toDemolish = null;
return response;
}
if (carpenterMenu != null && !carpenterMenu.CanDemolishThis(toDemolish))
{
toDemolish = null;
return response;
}
if (carpenterMenu != null && !Game1.IsMasterGame && !carpenterMenu.hasPermissionsToDemolish(toDemolish))
{
toDemolish = null;
return response;
}
}
if (toDemolish != null && toDemolish.indoors.Value is Cabin)
{
Cabin cabin = (Cabin)toDemolish.indoors.Value;
if (cabin.farmhand.Value != null && (bool)cabin.farmhand.Value.isCustomized)
{
Game1.currentLocation.createQuestionDialogue(Game1.content.LoadString("Strings\\UI:Carpenter_DemolishCabinConfirm", cabin.farmhand.Value.Name), Game1.currentLocation.createYesNoResponses(), delegate (Farmer f, string answer)
{
if (answer == "Yes")
{
Game1.activeClickableMenu = carpenterMenu;
Game1.player.team.demolishLock.RequestLock(continueDemolish, buildingLockFailed);
}
else
{
if (carpenterMenu != null)
DelayedAction.functionAfterDelay(carpenterMenu.returnToCarpentryMenu, 1000);
}
});
return response;
}
}
if (toDemolish != null)
{
Game1.player.team.demolishLock.RequestLock(continueDemolish, buildingLockFailed);
}
return response;
}
public static string? Contstruct(Vector2 position)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::874)
Game1.player.team.buildLock.RequestLock(delegate
{
if (isOnFarm && Game1.locationRequest == null)
{
if (tryToBuild(position))
{
if (carpenterMenu != null)
{
carpenterMenu.CurrentBlueprint.consumeResources();
DelayedAction.functionAfterDelay(carpenterMenu.returnToCarpentryMenuAfterSuccessfulBuild, 2000);
}
// freeze = true;
}
else
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantBuild");
}
}
Game1.player.team.buildLock.ReleaseLock();
});
return response;
}
public static bool tryToBuild(Vector2 position)
{
if (carpenterMenu == null)
return false;
return ((Farm)Game1.getLocationFromName("Farm")).buildStructure(carpenterMenu.CurrentBlueprint, position, Game1.player, isMagicalConstruction);
}
public static string? Upgrade(Building? toUpgrade)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::775)
if (carpenterMenu != null && toUpgrade != null && carpenterMenu.CurrentBlueprint.name != null && toUpgrade.buildingType.Equals(carpenterMenu.CurrentBlueprint.nameOfBuildingToUpgrade))
{
carpenterMenu.CurrentBlueprint.consumeResources();
toUpgrade.daysUntilUpgrade.Value = 2;
toUpgrade.showUpgradeAnimation(Game1.getFarm());
Game1.playSound("axe");
DelayedAction.functionAfterDelay(carpenterMenu.returnToCarpentryMenuAfterSuccessfulBuild, 1500);
// freeze = true;
// Game1.multiplayer.globalChatInfoMessage("BuildingBuild", Game1.player.Name, Utility.AOrAn(carpenterMenu.CurrentBlueprint.displayName), carpenterMenu.CurrentBlueprint.displayName, Game1.player.farmName);
}
else if (toUpgrade != null)
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CantUpgrade_BuildingType");
}
return response;
}
public static string? Paint(Building? toPaint)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::793)
Farm farm_location = Game1.getFarm();
if (toPaint != null)
{
if (!toPaint.CanBePainted())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint");
return response;
}
if (carpenterMenu != null && !carpenterMenu.HasPermissionsToPaint(toPaint))
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint_Permission");
return response;
}
toPaint.color.Value = Color.White;
if (carpenterMenu != null)
carpenterMenu.SetChildMenu(new BuildingPaintMenu(toPaint));
}
/* TODO Add painting of farm house
else if (farm_location.GetHouseRect().Contains(Utility.Vector2ToPoint(new Vector2(toPaint.tileX, toPaint.tileY))))
{
if (!carpenterMenu.CanPaintHouse())
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint");
}
else if (!carpenterMenu.HasPermissionsToPaint(null))
{
response = Game1.content.LoadString("Strings\\UI:Carpenter_CannotPaint_Permission");
}
else
{
carpenterMenu.SetChildMenu(new BuildingPaintMenu("House", () => (farm_location.paintedHouseTexture != null) ? farm_location.paintedHouseTexture : Farm.houseTextures, farm_location.houseSource.Value, farm_location.housePaintColor.Value));
}
}*/
return response;
}
public static string? Move(Building? buildingToMove, Vector2 position)
{
string? response = null;
// This code is taken from the game's code (CarpenterMenu.cs::829)
if (buildingToMove != null)
{
string? name = buildingToMove.nameOfIndoorsWithoutUnique;
name = (name == "null") ? buildingToMove.buildingType.Value : name;
if ((int)buildingToMove.daysOfConstructionLeft > 0)
{
buildingToMove = null;
return "Building under construction, cannot move";
}
if (carpenterMenu != null && !carpenterMenu.hasPermissionsToMove(buildingToMove))
{
buildingToMove = null;
return "You don't have permission to move this building";
}
Game1.playSound("axchop");
if (((Farm)Game1.getLocationFromName("Farm")).buildStructure(buildingToMove, position, Game1.player))
{
if (buildingToMove is ShippingBin)
{
((ShippingBin)buildingToMove).initLid();
}
if (buildingToMove is GreenhouseBuilding)
{
Game1.getFarm().greenhouseMoved.Value = true;
}
buildingToMove.performActionOnBuildingPlacement();
buildingToMove = null;
Game1.playSound("axchop");
DelayedAction.playSoundAfterDelay("dirtyHit", 50);
DelayedAction.playSoundAfterDelay("dirtyHit", 150);
response = $"{buildingToMove} moved to {position.X}x {position.Y}y";
}
else
{
Game1.playSound("cancel");
response = $"Cannot move building to {position.X}x {position.Y}y";
}
}
return response;
}
public static void PurchaseAnimal(Building? selection)
{
if (selection == null)
return;
if (purchaseAnimalsMenu == null)
return;
int x = (selection.tileX * Game1.tileSize) - Game1.viewport.X;
int y = (selection.tileY * Game1.tileSize) - Game1.viewport.Y;
purchaseAnimalsMenu.receiveLeftClick(x, y);
}
}
}

View File

@@ -0,0 +1,70 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class JojaCDMenuPatch
{
internal static string jojaCDMenuQuery = "";
internal static void DrawPatch(JojaCDMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
string toSpeak = "";
for (int i = 0; i < __instance.checkboxes.Count; i++)
{
ClickableComponent c = __instance.checkboxes[i];
if (!c.containsPoint(x, y))
continue;
if (c.name.Equals("complete"))
{
toSpeak = $"Completed {getNameFromIndex(i)}";
}
else
{
toSpeak = $"{getNameFromIndex(i)} Cost: {__instance.getPriceFromButtonNumber(i)}g Description: {__instance.getDescriptionFromButtonNumber(i)}";
}
break;
}
if (jojaCDMenuQuery != toSpeak)
{
jojaCDMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static string getNameFromIndex(int i)
{
string name = i switch
{
0 => "Bus",
1 => "Minecarts",
2 => "Bridge",
3 => "Greenhouse",
4 => "Panning",
_ => "",
};
if (name != "")
return $"{name} Project";
else
return "unkown";
}
internal static void Cleanup()
{
jojaCDMenuQuery = "";
}
}
}

View File

@@ -0,0 +1,297 @@
using StardewValley;
using StardewValley.Locations;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class JunimoNoteMenuPatch
{
internal static string junimoNoteMenuQuery = "";
internal static string currentJunimoArea = "";
internal static bool isUsingCustomKeyBinds = false;
internal static int currentIngredientListItem = -1, currentIngredientInputSlot = -1, currentInventorySlot = -1;
internal static void DrawPatch(JunimoNoteMenu __instance, bool ___specificBundlePage, int ___whichArea, Bundle ___currentPageBundle)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (narrateJunimoArea(__instance, ___specificBundlePage, ___whichArea, x, y))
{
return;
}
narrateBundlePage(__instance, ___specificBundlePage, ___currentPageBundle, x, y);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateJunimoArea(JunimoNoteMenu __instance, bool ___specificBundlePage, int ___whichArea, int x, int y)
{
if (___specificBundlePage)
return false;
currentIngredientListItem = -1;
isUsingCustomKeyBinds = false;
string areaName = __instance.scrambledText ? CommunityCenter.getAreaEnglishDisplayNameFromNumber(___whichArea) : CommunityCenter.getAreaDisplayNameFromNumber(___whichArea);
string reward = __instance.getRewardNameForArea(___whichArea);
if (__instance.scrambledText)
{
string scrambledText = "Scrambled Text";
if (junimoNoteMenuQuery != scrambledText)
{
junimoNoteMenuQuery = scrambledText;
MainClass.ScreenReader.Say(scrambledText, true);
}
return true;
}
if (currentJunimoArea != areaName)
{
currentJunimoArea = areaName;
MainClass.ScreenReader.Say($"Area {areaName}, {reward}", true);
return true;
}
string toSpeak = "";
if (__instance.presentButton != null && __instance.presentButton.containsPoint(x, y))
{
toSpeak = "Present Button";
}
else if (__instance.fromGameMenu && __instance.areaNextButton.visible && __instance.areaNextButton.containsPoint(x, y))
{
toSpeak = "Next Area Button";
}
else if (__instance.fromGameMenu && __instance.areaBackButton.visible && __instance.areaBackButton.containsPoint(x, y))
{
toSpeak = "Previous Area Button";
}
else
{
for (int i = 0; i < __instance.bundles.Count; i++)
{
if (!__instance.bundles[i].containsPoint(x, y))
continue;
toSpeak = $"{__instance.bundles[i].name} bundle";
break;
}
}
if (junimoNoteMenuQuery != toSpeak)
{
junimoNoteMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
return true;
}
return false;
}
private static void narrateBundlePage(JunimoNoteMenu __instance, bool ___specificBundlePage, Bundle ___currentPageBundle, int x, int y)
{
if (!___specificBundlePage)
return;
bool isIPressed = MainClass.Config.BundleMenuIngredientsKey.JustPressed(); // For the ingredients
bool isCPressed = MainClass.Config.BundleMenuInventoryItemsKey.JustPressed(); // For the items in inventory
bool isPPressed = MainClass.Config.BundleMenuPurchaseButtonKey.JustPressed(); // For the Purchase Button
bool isVPressed = MainClass.Config.BundleMenuIngredientsInputSlotKey.JustPressed(); // For the ingredient input slots
bool isBackPressed = MainClass.Config.BundleMenuBackButtonKey.JustPressed(); // For the back button
bool isLeftShiftPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftShift);
if (isIPressed && !isUsingCustomKeyBinds)
{
isUsingCustomKeyBinds = true;
cycleThroughIngredientList(__instance, ___currentPageBundle, isLeftShiftPressed);
Task.Delay(200).ContinueWith(_ => { isUsingCustomKeyBinds = false; });
}
else if (isVPressed && !isUsingCustomKeyBinds)
{
isUsingCustomKeyBinds = true;
cycleThroughInputSlots(__instance, ___currentPageBundle, isLeftShiftPressed);
Task.Delay(200).ContinueWith(_ => { isUsingCustomKeyBinds = false; });
}
else if (isCPressed && !isUsingCustomKeyBinds)
{
isUsingCustomKeyBinds = true;
cycleThroughInventorySlots(__instance, ___currentPageBundle, isLeftShiftPressed);
Task.Delay(200).ContinueWith(_ => { isUsingCustomKeyBinds = false; });
}
else if (isBackPressed && __instance.backButton != null && !__instance.backButton.containsPoint(x, y))
{
__instance.backButton.snapMouseCursorToCenter();
MainClass.ScreenReader.Say("Back Button", true);
}
else if (isPPressed && __instance.purchaseButton != null && !__instance.purchaseButton.containsPoint(x, y))
{
__instance.purchaseButton.snapMouseCursorToCenter();
MainClass.ScreenReader.Say("Purchase Button", true);
}
return;
}
private static void cycleThroughIngredientList(JunimoNoteMenu __instance, Bundle ___currentPageBundle, bool isLeftShiftPressed = false)
{
if (___currentPageBundle.ingredients.Count < 0)
return;
currentIngredientListItem = currentIngredientListItem + (isLeftShiftPressed ? -1 : 1);
if (currentIngredientListItem >= ___currentPageBundle.ingredients.Count)
if (isLeftShiftPressed)
currentIngredientListItem = ___currentPageBundle.ingredients.Count - 1;
else
currentIngredientListItem = 0;
if (currentIngredientListItem < 0)
if (isLeftShiftPressed)
currentIngredientListItem = ___currentPageBundle.ingredients.Count - 1;
else
currentIngredientListItem = 0;
ClickableTextureComponent c = __instance.ingredientList[currentIngredientListItem];
BundleIngredientDescription ingredient = ___currentPageBundle.ingredients[currentIngredientListItem];
Item item = new StardewValley.Object(ingredient.index, ingredient.stack, isRecipe: false, -1, ingredient.quality);
bool completed = false;
if (___currentPageBundle != null && ___currentPageBundle.ingredients != null && currentIngredientListItem < ___currentPageBundle.ingredients.Count && ___currentPageBundle.ingredients[currentIngredientListItem].completed)
{
completed = true;
}
string toSpeak = item.DisplayName;
if (completed)
{
toSpeak = $"Completed {toSpeak}";
}
else
{
int quality = ingredient.quality;
if (quality == 1)
{
toSpeak = $"Silver quality {toSpeak}";
}
else if (quality == 2 || quality == 3)
{
toSpeak = $"Gold quality {toSpeak}";
}
else if (quality >= 4)
{
toSpeak = $"Iridium quality {toSpeak}";
}
toSpeak = $"{ingredient.stack} {toSpeak}";
}
c.snapMouseCursorToCenter();
MainClass.ScreenReader.Say(toSpeak, true);
}
private static void cycleThroughInputSlots(JunimoNoteMenu __instance, Bundle ___currentPageBundle, bool isLeftShiftPressed = false)
{
if (__instance.ingredientSlots.Count < 0)
return;
currentIngredientInputSlot = currentIngredientInputSlot + (isLeftShiftPressed ? -1 : 1);
if (currentIngredientInputSlot >= __instance.ingredientSlots.Count)
if (isLeftShiftPressed)
currentIngredientInputSlot = __instance.ingredientSlots.Count - 1;
else
currentIngredientInputSlot = 0;
if (currentIngredientInputSlot < 0)
if (isLeftShiftPressed)
currentIngredientInputSlot = __instance.ingredientSlots.Count - 1;
else
currentIngredientInputSlot = 0;
ClickableTextureComponent c = __instance.ingredientSlots[currentIngredientInputSlot];
Item item = c.item;
string toSpeak;
if (item == null)
{
toSpeak = $"Input Slot {currentIngredientInputSlot + 1}";
}
else
{
toSpeak = item.DisplayName;
}
c.snapMouseCursorToCenter();
MainClass.ScreenReader.Say(toSpeak, true);
}
private static void cycleThroughInventorySlots(JunimoNoteMenu __instance, Bundle ___currentPageBundle, bool isLeftShiftPressed = false)
{
if (__instance.inventory == null || __instance.inventory.actualInventory.Count < 0)
return;
int prevSlotIndex = currentInventorySlot;
currentInventorySlot = currentInventorySlot + (isLeftShiftPressed ? -1 : 1);
if (currentInventorySlot >= __instance.inventory.actualInventory.Count)
if (isLeftShiftPressed)
currentInventorySlot = __instance.inventory.actualInventory.Count - 1;
else
currentInventorySlot = 0;
if (currentInventorySlot < 0)
if (isLeftShiftPressed)
currentInventorySlot = __instance.inventory.actualInventory.Count - 1;
else
currentInventorySlot = 0;
Item item = __instance.inventory.actualInventory[currentInventorySlot];
ClickableComponent c = __instance.inventory.inventory[currentInventorySlot];
string toSpeak;
if (item != null)
{
toSpeak = item.DisplayName;
if ((item as StardewValley.Object) != null)
{
int quality = ((StardewValley.Object)item).Quality;
if (quality == 1)
{
toSpeak = $"Silver quality {toSpeak}";
}
else if (quality == 2 || quality == 3)
{
toSpeak = $"Gold quality {toSpeak}";
}
else if (quality >= 4)
{
toSpeak = $"Iridium quality {toSpeak}";
}
}
toSpeak = $"{item.Stack} {toSpeak}";
if (!__instance.inventory.highlightMethod(__instance.inventory.actualInventory[currentInventorySlot]))
{
toSpeak = $"{toSpeak} not usable here";
}
}
else
{
toSpeak = "Empty Slot";
}
c.snapMouseCursorToCenter();
MainClass.ScreenReader.Say(toSpeak, true);
}
internal static void Cleanup()
{
JunimoNoteMenuPatch.currentIngredientListItem = -1;
JunimoNoteMenuPatch.currentIngredientInputSlot = -1;
JunimoNoteMenuPatch.currentInventorySlot = -1;
JunimoNoteMenuPatch.junimoNoteMenuQuery = "";
}
}
}

View File

@@ -1,85 +0,0 @@
using StardewModdingAPI;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ChatManuPatches
{
private static int currentChatMessageIndex = 0;
private static bool isChatRunning = false;
internal static void ChatBoxPatch(ChatBox __instance, List<ChatMessage> ___messages)
{
try
{
string toSpeak = " ";
if (__instance.chatBox.Selected)
{
bool isPrevArrowPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.PageUp);
bool isNextArrowPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.PageDown);
if (___messages.Count > 0)
{
#region To narrate previous and next chat messages
if (isNextArrowPressed && !isChatRunning)
{
CycleThroughChatMessages(true, ___messages);
}
else if (isPrevArrowPressed && !isChatRunning)
{
CycleThroughChatMessages(false, ___messages);
}
#endregion
}
}
else if (___messages.Count > 0)
{
#region To narrate latest chat message
___messages[___messages.Count - 1].message.ForEach(message =>
{
toSpeak += $"{message.message}, ";
});
if (toSpeak != " ")
MainClass.GetScreenReader().SayWithChatChecker(toSpeak, false);
#endregion
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
private static async void CycleThroughChatMessages(bool increase, List<ChatMessage> ___messages)
{
isChatRunning = true;
string toSpeak = " ";
if (increase)
{
++currentChatMessageIndex;
if (currentChatMessageIndex > ___messages.Count - 1)
{
currentChatMessageIndex = ___messages.Count - 1;
}
}
else
{
--currentChatMessageIndex;
if (currentChatMessageIndex < 0)
{
currentChatMessageIndex = 0;
}
}
___messages[currentChatMessageIndex].message.ForEach(message =>
{
toSpeak += $"{message.message}, ";
});
MainClass.GetScreenReader().Say(toSpeak, true);
await Task.Delay(200);
isChatRunning = false;
}
}
}

View File

@@ -1,322 +0,0 @@
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Menus;
using System.Text;
namespace stardew_access.Patches
{
internal class DialoguePatches
{
internal static string currentDialogue = " ";
internal static bool isDialogueAppearingFirstTime = true;
internal static void DialoguePatch(DialogueBox __instance, SpriteBatch b)
{
try
{
if (__instance.transitioning)
return;
if (__instance.characterDialogue != null)
{
// For Normal Character dialogues
Dialogue dialogue = __instance.characterDialogue;
string speakerName = dialogue.speaker.displayName;
List<Response> responses = __instance.responses;
string toSpeak = " ";
string dialogueText = "";
string response = "";
bool hasResponses = dialogue.isCurrentDialogueAQuestion();
dialogueText = $"{speakerName} said {__instance.getCurrentString()}";
if (hasResponses)
{
if (__instance.selectedResponse >= 0 && __instance.selectedResponse < responses.Count)
response = $"{__instance.selectedResponse + 1}: {responses[__instance.selectedResponse].responseText}";
else
// When the dialogue is not finished writing then the selectedResponse is <0 and this results
// in the first response not being detcted, so this sets the first response option to be the default
// if the current dialogue is a question or has responses
response = $"1: {responses[0].responseText}";
}
if (hasResponses)
{
if (currentDialogue != response)
{
currentDialogue = response;
if (isDialogueAppearingFirstTime)
{
toSpeak = $"{dialogueText} \n\t {response}";
isDialogueAppearingFirstTime = false;
}
else
toSpeak = response;
MainClass.GetScreenReader().Say(toSpeak, true);
}
}
else
{
if (currentDialogue != dialogueText)
{
currentDialogue = dialogueText;
MainClass.GetScreenReader().Say(dialogueText, true);
}
}
}
else if (__instance.isQuestion)
{
// For Dialogues with responses/answers like the dialogue when we click on tv
string toSpeak = "";
string dialogueText = "";
string response = "";
bool hasResponses = false;
if (__instance.responses.Count > 0)
hasResponses = true;
dialogueText = __instance.getCurrentString();
if (hasResponses)
if (__instance.selectedResponse >= 0 && __instance.selectedResponse < __instance.responses.Count)
response = $"{__instance.selectedResponse + 1}: {__instance.responses[__instance.selectedResponse].responseText}";
else
// When the dialogue is not finished writing then the selectedResponse is <0 and this results
// in the first response not being detcted, so this sets the first response option to be the default
// if the current dialogue is a question or has responses
response = $"1: {__instance.responses[0].responseText}";
if (hasResponses)
{
if (currentDialogue != response)
{
currentDialogue = response;
if (isDialogueAppearingFirstTime)
{
toSpeak = $"{dialogueText} \n\t {response}";
isDialogueAppearingFirstTime = false;
}
else
toSpeak = response;
MainClass.GetMonitor().Log(toSpeak, LogLevel.Debug);
MainClass.GetScreenReader().Say(toSpeak, true);
}
}
else
{
if (currentDialogue != dialogueText)
{
currentDialogue = dialogueText;
MainClass.GetScreenReader().Say(dialogueText, true);
}
}
}
else if (Game1.activeClickableMenu is DialogueBox)
{
// Basic dialogues like `No mails in the mail box`
if (currentDialogue != __instance.getCurrentString())
{
currentDialogue = __instance.getCurrentString();
MainClass.GetScreenReader().Say(__instance.getCurrentString(), true);
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}", LogLevel.Error);
}
}
internal static void ClearDialogueString()
{
// CLears the currentDialogue string on closing dialog
currentDialogue = " ";
isDialogueAppearingFirstTime = true;
}
internal static void HoverTextPatch(string? text, int moneyAmountToDisplayAtBottom = -1, string? boldTitleText = null, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, string[]? buffIconsToDisplay = null, Item? hoveredItem = null, CraftingRecipe? craftingIngredients = null)
{
try
{
#region Skip narrating hover text for certain menus
if (Game1.activeClickableMenu is TitleMenu && !(((TitleMenu)Game1.activeClickableMenu).GetChildMenu() is CharacterCustomization))
return;
if (Game1.activeClickableMenu is LetterViewerMenu || Game1.activeClickableMenu is QuestLog)
return;
if (Game1.activeClickableMenu is Billboard)
return;
if (Game1.activeClickableMenu is GeodeMenu)
return;
if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is InventoryPage)
return;
if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is CraftingPage)
return;
if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is OptionsPage)
return;
if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is ExitPage)
return;
if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is SocialPage)
return;
if (Game1.activeClickableMenu is ItemGrabMenu)
return;
if (Game1.activeClickableMenu is ShopMenu)
return;
if (Game1.activeClickableMenu is ConfirmationDialog)
return;
if (Game1.activeClickableMenu is JunimoNoteMenu)
return;
if (Game1.activeClickableMenu is CarpenterMenu)
return;
if (Game1.activeClickableMenu is PurchaseAnimalsMenu)
return;
#endregion
StringBuilder toSpeak = new StringBuilder(" ");
#region Add item count before title
if (hoveredItem != null && hoveredItem.HasBeenInInventory)
{
int count = hoveredItem.Stack;
if (count > 1)
toSpeak.Append($"{count} ");
}
#endregion
#region Add title if any
if (boldTitleText != null)
toSpeak.Append($"{boldTitleText}\n");
#endregion
#region Add quality of item
if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).quality > 0)
{
int quality = ((StardewValley.Object)hoveredItem).quality;
if (quality == 1)
{
toSpeak.Append("Silver quality");
}
else if (quality == 2 || quality == 3)
{
toSpeak.Append("Gold quality");
}
else if (quality >= 4)
{
toSpeak.Append("Iridium quality");
}
}
#endregion
#region Narrate hovered required ingredients
if (extraItemToShowIndex != -1)
{
string itemName = Game1.objectInformation[extraItemToShowIndex].Split('/')[0];
if (extraItemToShowAmount != -1)
toSpeak.Append($"Required: {extraItemToShowAmount} {itemName}");
else
toSpeak.Append($"Required: {itemName}");
}
#endregion
#region Add money
if (moneyAmountToDisplayAtBottom != -1)
toSpeak.Append($"\nCost: {moneyAmountToDisplayAtBottom}g\n");
#endregion
#region Add the base text
toSpeak.Append(text);
#endregion
#region Add crafting ingredients
if (craftingIngredients != null)
{
toSpeak.Append($"\n{craftingIngredients.description}");
toSpeak.Append("\nIngredients\n");
craftingIngredients.recipeList.ToList().ForEach(recipe =>
{
int count = recipe.Value;
int item = recipe.Key;
string name = craftingIngredients.getNameFromIndex(item);
toSpeak.Append($" ,{count} {name}");
});
}
#endregion
#region Add health & stamina
if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Edibility != -300)
{
int stamina_recovery = ((StardewValley.Object)hoveredItem).staminaRecoveredOnConsumption();
toSpeak.Append($"{stamina_recovery} Energy\n");
if (stamina_recovery >= 0)
{
int health_recovery = ((StardewValley.Object)hoveredItem).healthRecoveredOnConsumption();
toSpeak.Append($"{health_recovery} Health");
}
}
#endregion
#region Add buff items (effects like +1 walking speed)
if (buffIconsToDisplay != null)
{
for (int i = 0; i < buffIconsToDisplay.Length; i++)
{
string buffName = ((Convert.ToInt32(buffIconsToDisplay[i]) > 0) ? "+" : "") + buffIconsToDisplay[i] + " ";
if (i <= 11)
{
buffName = Game1.content.LoadString("Strings\\UI:ItemHover_Buff" + i, buffName);
}
try
{
int count = int.Parse(buffName.Substring(0, buffName.IndexOf(' ')));
if (count != 0)
toSpeak.Append($"{buffName}\n");
}
catch (Exception) { }
}
}
#endregion
#region Narrate toSpeak
// To prevent it from getting conflicted by two hover texts at the same time, two seperate methods are used.
// For example, sometimes `Welcome to Pierre's` and the items in seeds shop get conflicted causing it to speak infinitely.
if (toSpeak.ToString() != " ")
{
if (Context.IsPlayerFree)
MainClass.GetScreenReader().SayWithChecker(toSpeak.ToString(), true); // Normal Checker
else
MainClass.GetScreenReader().SayWithMenuChecker(toSpeak.ToString(), true); // Menu Checker
}
#endregion
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}", LogLevel.Error);
}
}
}
}

View File

@@ -0,0 +1,123 @@
using StardewValley;
using stardew_access.Features;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class FieldOfficeMenuPatch
{
private static string fieldOfficeMenuQuery = "";
internal static void DrawPatch(FieldOfficeMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
string toSpeak = " ";
if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trashcan";
}
else if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "ok button";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "drop item";
}
else
{
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y))
return;
for (int i = 0; i < __instance.pieceHolders.Count; i++)
{
if (!__instance.pieceHolders[i].containsPoint(x, y))
continue;
if (__instance.pieceHolders[i].item == null)
toSpeak = i switch
{
0 => "Center skeleton slot",
1 => "Center skeleton slot",
2 => "Center skeleton slot",
3 => "Center skeleton slot",
4 => "Center skeleton slot",
5 => "Center skeleton slot",
6 => "Snake slot",
7 => "Snake slot",
8 => "Snake slot",
9 => "Bat slot",
10 => "Frog slot",
_ => "Donation slot"
};
else
toSpeak = $"Slot {i + 1} finished: {__instance.pieceHolders[i].item.DisplayName}";
if (!MainClass.Config.DisableInventoryVerbosity && __instance.heldItem != null && __instance.pieceHolders[i].item == null)
{
int highlight = getPieceIndexForDonationItem(__instance.heldItem.ParentSheetIndex);
if (highlight != -1 && highlight == i)
toSpeak += "Donatable ";
}
if (fieldOfficeMenuQuery != $"{toSpeak}:{i}")
{
fieldOfficeMenuQuery = $"{toSpeak}:{i}";
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
}
if (fieldOfficeMenuQuery != toSpeak)
{
fieldOfficeMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
Game1.playSound("drop_item");
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static int getPieceIndexForDonationItem(int itemIndex)
{
switch (itemIndex)
{
case 820:
return 5;
case 821:
return 4;
case 822:
return 3;
case 823:
return 0;
case 824:
return 1;
case 825:
return 8;
case 826:
return 7;
case 827:
return 9;
case 828:
return 10;
default:
return -1;
}
}
internal static void Cleanup()
{
fieldOfficeMenuQuery = "";
}
}
}

View File

@@ -0,0 +1,251 @@
using Microsoft.Xna.Framework;
using StardewValley;
using stardew_access.Features;
using StardewValley.Locations;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class MuseumMenuPatch
{
private static string museumQueryKey = "";
private static bool isMoving = false;
private static (int x, int y)[] donationTiles =
{
(26,5),(26,6),(26,7),(26,8),(26,9),(26,10),(26,11),
(29,5),(30,5),(31,5),(32,5),(33,5),(34,5),(35,5),(36,5),
(28,6),(29,6),(30,6),(31,6),(32,6),(33,6),(34,6),(35,6),(36,6),(37,6),
(28,9),(29,9),(30,9),(31,9),(32,9),(33,9),(34,9),(35,9),(36,9),
(28,10),(29,10),(30,10),(31,10),(32,10),(33,10),(34,10),(35,10),(36,10),
(30,13),(31,13),(32,13),(33,13),(34,13),
(30,14),(31,14),(32,14),(33,14),(34,14),
(28,15),(29,15),(30,15),(31,15),(32,15),(33,15),(34,15),(35,15),(36,15),
(28,16),(29,16),(30,16),(31,16),(32,16),(33,16),(34,16),(35,16),(36,16),
(39,6),(40,6),(41,6),(42,6),(43,6),(44,6),(45,6),(46,6),
(39,7),(40,7),(41,7),(42,7),(43,7),(44,7),(45,7),(46,7),
(48,5),(48,6),(48,7),
(42,15),(43,15),(44,15),(45,15),(46,15),(47,15),
(42,16),(43,16),(44,16),(45,16),(46,16),(47,16),
};
internal static bool RecieveKeyPressPatch()
{
try
{
if (isMoving)
return false;
if (!isMoving)
{
isMoving = true;
Task.Delay(200).ContinueWith(_ => { isMoving = false; });
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
return true;
}
internal static void DrawPatch(MuseumMenu __instance, bool ___holdingMuseumPiece)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
narrateMuseumInventory(__instance, x, y);
narratePlayerInventory(__instance, x, y);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static void narrateMuseumInventory(MuseumMenu __instance, int x, int y)
{
if (__instance.heldItem == null) return;
string toSpeak = "";
int tileX = (int)(Utility.ModifyCoordinateFromUIScale(x) + (float)Game1.viewport.X) / 64;
int tileY = (int)(Utility.ModifyCoordinateFromUIScale(y) + (float)Game1.viewport.Y) / 64;
LibraryMuseum libraryMuseum = (LibraryMuseum)Game1.currentLocation;
if (libraryMuseum.isTileSuitableForMuseumPiece(tileX, tileY))
toSpeak = $"slot {tileX}x {tileY}y";
if (museumQueryKey != toSpeak)
{
museumQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
private static void narratePlayerInventory(MuseumMenu __instance, int x, int y)
{
if (__instance.heldItem != null) return;
if (narrateHoveredButtons(__instance, x, y)) return;
int hoveredItemIndex = InventoryUtils.narrateHoveredSlotAndReturnIndex(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y,
handleHighlightedItem: true, highlightedItemPrefix: "Donatable ");
if (hoveredItemIndex != -9999)
{
bool isPrimaryInfoKeyPressed = MainClass.Config.PrimaryInfoKey.JustPressed(); // For donating hovered item
if (isPrimaryInfoKeyPressed && hoveredItemIndex >= 0 && hoveredItemIndex < __instance.inventory.actualInventory.Count && __instance.inventory.actualInventory[hoveredItemIndex] != null)
{
manuallyDonateItem(__instance, hoveredItemIndex);
}
}
}
private static bool narrateHoveredButtons(MuseumMenu __instance, int x, int y)
{
string toSpeak = "";
bool isDropItemButton = false;
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "Ok button";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "Drop Item";
isDropItemButton = true;
}
else
{
return false;
}
if (museumQueryKey != toSpeak)
{
museumQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
}
return true;
}
private static void manuallyDonateItem(MuseumMenu __instance, int i)
{
foreach (var tile in donationTiles)
{
#region Manually donates the hovered item (https://github.com/veywrn/StardewValley/blob/3ff171b6e9e6839555d7881a391b624ccd820a83/StardewValley/Menus/MuseumMenu.cs#L206-L247)
int tileX = tile.x;
int tileY = tile.y;
if (((LibraryMuseum)Game1.currentLocation).isTileSuitableForMuseumPiece(tileX, tileY) && ((LibraryMuseum)Game1.currentLocation).isItemSuitableForDonation(__instance.inventory.actualInventory[i]))
{
int objectID = __instance.inventory.actualInventory[i].ParentSheetIndex;
int rewardsCount = ((LibraryMuseum)Game1.currentLocation).getRewardsForPlayer(Game1.player).Count;
((LibraryMuseum)Game1.currentLocation).museumPieces.Add(new Vector2(tileX, tileY), ((StardewValley.Object)__instance.inventory.actualInventory[i]).ParentSheetIndex);
Game1.playSound("stoneStep");
if (((LibraryMuseum)Game1.currentLocation).getRewardsForPlayer(Game1.player).Count > rewardsCount)
{
Game1.playSound("reward");
}
else
{
Game1.playSound("newArtifact");
}
Game1.player.completeQuest(24);
__instance.inventory.actualInventory[i].Stack--;
if (__instance.inventory.actualInventory[i].Stack <= 0)
{
__instance.inventory.actualInventory[i] = null;
}
int pieces = ((LibraryMuseum)Game1.currentLocation).museumPieces.Count();
Game1.stats.checkForArchaeologyAchievements();
switch (pieces)
{
case 95:
globalChatInfoMessage("MuseumComplete", Game1.player.farmName.Value);
break;
case 40:
globalChatInfoMessage("Museum40", Game1.player.farmName.Value);
break;
default:
globalChatInfoMessage("donation", Game1.player.Name, "object:" + objectID);
break;
}
break;
}
#endregion
}
}
#region These methods are taken from the game's source code, https://github.com/veywrn/StardewValley/blob/3ff171b6e9e6839555d7881a391b624ccd820a83/StardewValley/Multiplayer.cs#L1331-L1395
internal static void globalChatInfoMessage(string messageKey, params string[] args)
{
if (Game1.IsMultiplayer || Game1.multiplayerMode != 0)
{
receiveChatInfoMessage(Game1.player, messageKey, args);
sendChatInfoMessage(messageKey, args);
}
}
internal static void sendChatInfoMessage(string messageKey, params string[] args)
{
if (Game1.IsClient)
{
Game1.client.sendMessage(15, messageKey, args);
}
else if (Game1.IsServer)
{
foreach (long id in Game1.otherFarmers.Keys)
{
Game1.server.sendMessage(id, 15, Game1.player, messageKey, args);
}
}
}
internal static void receiveChatInfoMessage(Farmer sourceFarmer, string messageKey, string[] args)
{
if (Game1.chatBox != null)
{
try
{
string[] processedArgs = args.Select(delegate (string arg)
{
if (arg.StartsWith("achievement:"))
{
int key = Convert.ToInt32(arg.Substring("achievement:".Length));
return Game1.content.Load<Dictionary<int, string>>("Data\\Achievements")[key].Split('^')[0];
}
return arg.StartsWith("object:") ? new StardewValley.Object(Convert.ToInt32(arg.Substring("object:".Length)), 1).DisplayName : arg;
}).ToArray();
ChatBox chatBox = Game1.chatBox;
LocalizedContentManager content = Game1.content;
string path = "Strings\\UI:Chat_" + messageKey;
object[] substitutions = processedArgs;
chatBox.addInfoMessage(content.LoadString(path, substitutions));
}
catch (Microsoft.Xna.Framework.Content.ContentLoadException)
{
}
catch (FormatException)
{
}
catch (OverflowException)
{
}
catch (KeyNotFoundException)
{
}
}
}
#endregion
internal static void Cleanup()
{
museumQueryKey = "";
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
namespace stardew_access.Patches
{
internal class CollectionsPagePatch
{
internal static void DrawPatch(StardewValley.Menus.CollectionsPage __instance)
{
try
{
int x = StardewValley.Game1.getMousePosition().X, y = StardewValley.Game1.getMousePosition().Y;
if (__instance.letterviewerSubMenu != null)
{
LetterViwerMenuPatch.narrateLetterContent(__instance.letterviewerSubMenu);
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,251 @@
using StardewValley;
using stardew_access.Features;
using StardewValley.Menus;
using StardewValley.Objects;
namespace stardew_access.Patches
{
internal class CraftingPagePatch
{
internal static string hoveredItemQueryKey = "";
internal static string craftingPageQueryKey = "";
internal static int currentSelectedCraftingRecipe = -1;
internal static bool isSelectingRecipe = false;
internal static void DrawPatch(CraftingPage __instance, CraftingRecipe ___hoverRecipe, int ___currentCraftingPage)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
handleKeyBinds(__instance, ___currentCraftingPage);
if (narrateMenuButtons(__instance, x, y))
{
return;
}
if (narrateHoveredRecipe(__instance, ___currentCraftingPage, ___hoverRecipe, x, y))
{
return;
}
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y))
{
craftingPageQueryKey = "";
return;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static void handleKeyBinds(CraftingPage __instance, int ___currentCraftingPage)
{
if (MainClass.Config.SnapToFirstSecondaryInventorySlotKey.JustPressed() && __instance.pagesOfCraftingRecipes[___currentCraftingPage].Count > 0)
{
// snap to first crafting recipe
__instance.setCurrentlySnappedComponentTo(__instance.pagesOfCraftingRecipes[___currentCraftingPage].ElementAt(0).Key.myID);
__instance.pagesOfCraftingRecipes[___currentCraftingPage].ElementAt(0).Key.snapMouseCursorToCenter();
currentSelectedCraftingRecipe = 0;
}
else if (MainClass.Config.SnapToFirstInventorySlotKey.JustPressed() && __instance.inventory.inventory.Count > 0)
{
// snap to first inventory slot
__instance.setCurrentlySnappedComponentTo(__instance.inventory.inventory[0].myID);
__instance.inventory.inventory[0].snapMouseCursorToCenter();
currentSelectedCraftingRecipe = -1;
}
else if (MainClass.Config.CraftingMenuCycleThroughRecipiesKey.JustPressed() && !isSelectingRecipe)
{
isSelectingRecipe = true;
CycleThroughRecipies(__instance.pagesOfCraftingRecipes, ___currentCraftingPage, __instance);
Task.Delay(200).ContinueWith(_ => { isSelectingRecipe = false; });
}
}
private static bool narrateMenuButtons(CraftingPage __instance, int x, int y)
{
string? toSpeak = null;
bool isDropItemButton = false;
if (__instance.upButton != null && __instance.upButton.containsPoint(x, y))
{
toSpeak = "Previous Recipe List";
}
else if (__instance.downButton != null && __instance.downButton.containsPoint(x, y))
{
toSpeak = "Next Recipe List";
}
else if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trash Can";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "Drop Item";
isDropItemButton = true;
}
else
{
return false;
}
if (toSpeak != null && craftingPageQueryKey != toSpeak)
{
craftingPageQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
}
return true;
}
private static bool narrateHoveredRecipe(CraftingPage __instance, int ___currentCraftingPage, CraftingRecipe ___hoverRecipe, int x, int y)
{
if (___hoverRecipe == null)
{
var isRecipeInFocus = false;
foreach (var item in __instance.pagesOfCraftingRecipes[___currentCraftingPage])
{
if (!item.Key.containsPoint(x, y))
continue;
isRecipeInFocus = true;
break;
}
if (!isRecipeInFocus)
return false;
string query = $"unknown recipe:{__instance.getCurrentlySnappedComponent().myID}";
if (craftingPageQueryKey != query)
{
craftingPageQueryKey = query;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say("unknown recipe", true);
}
return true;
}
string name = ___hoverRecipe.DisplayName;
int numberOfProduce = ___hoverRecipe.numberProducedPerCraft;
string description = "";
string ingredients = "";
string buffs = "";
string craftable = "";
description = $"Description:\n{___hoverRecipe.description}";
craftable = ___hoverRecipe.doesFarmerHaveIngredientsInInventory(getContainerContents(__instance._materialContainers)) ? "Craftable" : "Not Craftable";
#region Crafting ingredients
ingredients = "Ingredients:\n";
for (int i = 0; i < ___hoverRecipe.recipeList.Count; i++)
{
int recipeCount = ___hoverRecipe.recipeList.ElementAt(i).Value;
int recipeItem = ___hoverRecipe.recipeList.ElementAt(i).Key;
string recipeName = ___hoverRecipe.getNameFromIndex(recipeItem);
ingredients += $" ,{recipeCount} {recipeName}";
}
#endregion
#region Health & stamina and buff items (effects like +1 walking speed)
Item producesItem = ___hoverRecipe.createItem();
if (producesItem is StardewValley.Object producesItemObject)
{
if (producesItemObject.Edibility != -300)
{
int stamina_recovery = producesItemObject.staminaRecoveredOnConsumption();
buffs += $"{stamina_recovery} Energy";
if (stamina_recovery >= 0)
{
int health_recovery = producesItemObject.healthRecoveredOnConsumption();
buffs += $"\n{health_recovery} Health";
}
}
// These variables are taken from the game's code itself (IClickableMenu.cs -> 1016 line)
bool edibleItem = producesItem != null && (int)producesItemObject.Edibility != -300;
string[]? buffIconsToDisplay = (producesItem != null && edibleItem && Game1.objectInformation[producesItemObject.ParentSheetIndex].Split('/').Length > 7)
? producesItem.ModifyItemBuffs(Game1.objectInformation[producesItemObject.ParentSheetIndex].Split('/')[7].Split(' '))
: null;
if (buffIconsToDisplay != null)
{
for (int j = 0; j < buffIconsToDisplay.Length; j++)
{
string buffName = ((Convert.ToInt32(buffIconsToDisplay[j]) > 0) ? "+" : "") + buffIconsToDisplay[j] + " ";
if (j <= 11)
{
buffName = Game1.content.LoadString("Strings\\UI:ItemHover_Buff" + j, buffName);
}
try
{
int count = int.Parse(buffName.Substring(0, buffName.IndexOf(' ')));
if (count != 0)
buffs += $"{buffName}\n";
}
catch (Exception) { }
}
buffs = $"Buffs and boosts:\n {buffs}";
}
}
#endregion
string toSpeak = $"{numberOfProduce} {name}, {craftable}, \n\t{ingredients}, \n\t{description} \n\t{buffs}";
if (craftingPageQueryKey != toSpeak)
{
craftingPageQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
}
return true;
}
private static void CycleThroughRecipies(List<Dictionary<ClickableTextureComponent, CraftingRecipe>> pagesOfCraftingRecipes, int ___currentCraftingPage, CraftingPage __instance)
{
currentSelectedCraftingRecipe++;
if (currentSelectedCraftingRecipe < 0 || currentSelectedCraftingRecipe >= pagesOfCraftingRecipes[0].Count)
currentSelectedCraftingRecipe = 0;
__instance.setCurrentlySnappedComponentTo(pagesOfCraftingRecipes[___currentCraftingPage].ElementAt(currentSelectedCraftingRecipe).Key.myID);
pagesOfCraftingRecipes[___currentCraftingPage].ElementAt(currentSelectedCraftingRecipe).Key.snapMouseCursorToCenter();
// Skip if recipe is not unlocked/unknown
if (pagesOfCraftingRecipes[___currentCraftingPage].ElementAt(currentSelectedCraftingRecipe).Key.hoverText.Equals("ghosted"))
CycleThroughRecipies(pagesOfCraftingRecipes, ___currentCraftingPage, __instance);
}
// This method is used to get the inventory items to check if the player has enough ingredients for a recipe
// Taken from CraftingPage.cs -> 169 line
internal static IList<Item>? getContainerContents(List<Chest> materialContainers)
{
if (materialContainers == null)
{
return null;
}
List<Item> items = new List<Item>();
for (int i = 0; i < materialContainers.Count; i++)
{
items.AddRange(materialContainers[i].items);
}
return items;
}
internal static void Cleanup()
{
hoveredItemQueryKey = "";
craftingPageQueryKey = "";
currentSelectedCraftingRecipe = -1;
isSelectingRecipe = false;
}
}
}

View File

@@ -0,0 +1,48 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ExitPagePatch
{
internal static string exitPageQueryKey = "";
internal static void DrawPatch(ExitPage __instance)
{
try
{
if (__instance.exitToTitle.visible &&
__instance.exitToTitle.containsPoint(Game1.getMouseX(true), Game1.getMouseY(true)))
{
string toSpeak = "Exit to Title Button";
if (exitPageQueryKey != toSpeak)
{
exitPageQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
if (__instance.exitToDesktop.visible &&
__instance.exitToDesktop.containsPoint(Game1.getMouseX(true), Game1.getMouseY(true)))
{
string toSpeak = "Exit to Desktop Button";
if (exitPageQueryKey != toSpeak)
{
exitPageQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
exitPageQueryKey = "";
}
}
}

View File

@@ -0,0 +1,48 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class GameMenuPatch
{
internal static string gameMenuQueryKey = "";
internal static void DrawPatch(GameMenu __instance)
{
try
{
// Skip if in map page
if (__instance.currentTab == 3)
return;
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
for (int i = 0; i < __instance.tabs.Count; i++)
{
if (!__instance.tabs[i].containsPoint(x, y))
continue;
string toSpeak = $"{GameMenu.getLabelOfTabFromIndex(i)} Tab" + ((i == __instance.currentTab) ? " Active" : "");
if (gameMenuQueryKey != toSpeak)
{
gameMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
// If not hovering on any tab button
Cleanup();
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
gameMenuQueryKey = "";
}
}
}

View File

@@ -0,0 +1,157 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class InventoryPagePatch
{
internal static string inventoryPageQueryKey = "";
internal static string hoveredItemQueryKey = "";
internal static void DrawPatch(InventoryPage __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
handleKeyBinds();
if (narrateHoveredButton(__instance, x, y))
{
return;
}
if (narrateHoveredEquipmentSlot(__instance, x, y))
{
return;
}
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y, true))
{
inventoryPageQueryKey = "";
return;
}
// If no slot or button is hovered
Cleanup();
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in InventoryPagePatch()->DrawPatch():\n{e.Message}\n{e.StackTrace}");
}
}
private static void handleKeyBinds()
{
if (!MainClass.Config.MoneyKey.JustPressed())
return;
string farmName = Game1.content.LoadString("Strings\\UI:Inventory_FarmName", Game1.player.farmName.Value);
string currentFunds = Game1.content.LoadString("Strings\\UI:Inventory_CurrentFunds" + (Game1.player.useSeparateWallets ? "_Separate" : ""), Utility.getNumberWithCommas(Game1.player.Money));
string totalEarnings = Game1.content.LoadString("Strings\\UI:Inventory_TotalEarnings" + (Game1.player.useSeparateWallets ? "_Separate" : ""), Utility.getNumberWithCommas((int)Game1.player.totalMoneyEarned));
int festivalScore = Game1.player.festivalScore;
int walnut = Game1.netWorldState.Value.GoldenWalnuts.Value;
int qiGems = Game1.player.QiGems;
int qiCoins = Game1.player.clubCoins;
string toSpeak = $"{farmName}\n{currentFunds}\n{totalEarnings}";
if (festivalScore > 0)
toSpeak = $"{toSpeak}\nFestival Score: {festivalScore}";
if (walnut > 0)
toSpeak = $"{toSpeak}\nGolden Walnut: {walnut}";
if (qiGems > 0)
toSpeak = $"{toSpeak}\nQi Gems: {qiGems}";
if (qiCoins > 0)
toSpeak = $"{toSpeak}\nQi Club Coins: {qiCoins}";
MainClass.ScreenReader.Say(toSpeak, true);
}
private static bool narrateHoveredButton(InventoryPage __instance, int x, int y)
{
string? toSpeak = null;
bool isDropItemButton = false;
if (__instance.inventory.dropItemInvisibleButton != null && __instance.inventory.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "Drop Item";
isDropItemButton = true;
}
else if (__instance.organizeButton != null && __instance.organizeButton.containsPoint(x, y))
{
toSpeak = "Organize Inventory Button";
}
else if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trash Can";
}
else if (__instance.organizeButton != null && __instance.organizeButton.containsPoint(x, y))
{
toSpeak = "Organize Button";
}
else if (__instance.junimoNoteIcon != null && __instance.junimoNoteIcon.containsPoint(x, y))
{
toSpeak = "Community Center Button";
}
else
{
return false;
}
if (toSpeak != null && inventoryPageQueryKey != toSpeak)
{
inventoryPageQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
}
return true;
}
private static bool narrateHoveredEquipmentSlot(InventoryPage __instance, int mouseX, int mouseY)
{
for (int i = 0; i < __instance.equipmentIcons.Count; i++)
{
if (!__instance.equipmentIcons[i].containsPoint(mouseX, mouseY))
continue;
string toSpeak = getNameAndDescriptionOfItem(__instance.equipmentIcons[i].name);
if (inventoryPageQueryKey != toSpeak)
{
inventoryPageQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
}
return true;
}
return false;
}
private static string getNameAndDescriptionOfItem(string slotName) => slotName switch
{
"Hat" => (Game1.player.hat.Value != null) ? $"{Game1.player.hat.Value.DisplayName}, {Game1.player.hat.Value.getDescription()}" : "Hat slot",
"Left Ring" => (Game1.player.leftRing.Value != null) ? $"{Game1.player.leftRing.Value.DisplayName}, {Game1.player.leftRing.Value.getDescription()}" : "Left Ring slot",
"Right Ring" => (Game1.player.rightRing.Value != null) ? $"{Game1.player.rightRing.Value.DisplayName}, {Game1.player.rightRing.Value.getDescription()}" : "Right ring slot",
"Boots" => (Game1.player.boots.Value != null) ? $"{Game1.player.boots.Value.DisplayName}, {Game1.player.boots.Value.getDescription()}" : "Boots slot",
"Shirt" => (Game1.player.shirtItem.Value != null) ? $"{Game1.player.shirtItem.Value.DisplayName}, {Game1.player.shirtItem.Value.getDescription()}" : "Shirt slot",
"Pants" => (Game1.player.pantsItem.Value != null) ? $"{Game1.player.pantsItem.Value.DisplayName}, {Game1.player.pantsItem.Value.getDescription()}" : "Pants slot",
_ => "unkown slot"
};
internal static void Cleanup()
{
InventoryUtils.Cleanup();
inventoryPageQueryKey = "";
hoveredItemQueryKey = "";
}
}
}

View File

@@ -0,0 +1,67 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class OptionsPagePatch
{
internal static string optionsPageQueryKey = "";
internal static void DrawPatch(OptionsPage __instance)
{
try
{
int currentItemIndex = Math.Max(0, Math.Min(__instance.options.Count - 7, __instance.currentItemIndex));
int x = Game1.getMouseX(true), y = Game1.getMouseY(true);
for (int i = 0; i < __instance.optionSlots.Count; i++)
{
if (!__instance.optionSlots[i].bounds.Contains(x, y) || currentItemIndex + i >= __instance.options.Count || !__instance.options[currentItemIndex + i].bounds.Contains(x - __instance.optionSlots[i].bounds.X, y - __instance.optionSlots[i].bounds.Y))
continue;
OptionsElement optionsElement = __instance.options[currentItemIndex + i];
string toSpeak = optionsElement.label;
if (optionsElement is OptionsButton)
toSpeak = $" {toSpeak} Button";
else if (optionsElement is OptionsCheckbox)
toSpeak = (((OptionsCheckbox)optionsElement).isChecked ? "Enabled" : "Disabled") + $" {toSpeak} Checkbox";
else if (optionsElement is OptionsDropDown)
toSpeak = $"{toSpeak} Dropdown, option {((OptionsDropDown)optionsElement).dropDownDisplayOptions[((OptionsDropDown)optionsElement).selectedOption]} selected";
else if (optionsElement is OptionsSlider)
toSpeak = $"{((OptionsSlider)optionsElement).value}% {toSpeak} Slider";
else if (optionsElement is OptionsPlusMinus)
toSpeak = $"{((OptionsPlusMinus)optionsElement).displayOptions[((OptionsPlusMinus)optionsElement).selected]} selected of {toSpeak}";
else if (optionsElement is OptionsInputListener)
{
string buttons = "";
((OptionsInputListener)optionsElement).buttonNames.ForEach(name => { buttons += $", {name}"; });
toSpeak = $"{toSpeak} is bound to {buttons}. Left click to change.";
}
else
{
if (toSpeak.Contains(":"))
toSpeak = toSpeak.Replace(":", "");
toSpeak = $"{toSpeak} Options:";
}
if (optionsPageQueryKey != toSpeak)
{
optionsPageQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
optionsPageQueryKey = "";
}
}
}

View File

@@ -0,0 +1,166 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class SocialPagePatch
{
internal static string socialPageQuery = "";
internal static void DrawPatch(SocialPage __instance, List<ClickableTextureComponent> ___sprites, int ___slotPosition, List<string> ___kidsNames)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
for (int i = ___slotPosition; i < ___slotPosition + 5; i++)
{
if (i >= ___sprites.Count)
continue;
if (__instance.names[i] is string && narrateNPCDetails(__instance, i, ___kidsNames, x, y))
{
return;
}
else if (__instance.names[i] is long && narrateFarmerDetails(__instance, i, ___sprites, x, y))
{
return;
}
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateNPCDetails(SocialPage __instance, int i, List<string> ___kidsNames, int x, int y)
{
if (!__instance.characterSlots[i].bounds.Contains(x, y))
return false;
string name = $"{__instance.names[i] as string}";
int heartLevel = Game1.player.getFriendshipHeartLevelForNPC(name);
bool datable = SocialPage.isDatable(name);
Friendship friendship = __instance.getFriendship(name);
int giftsThisWeek = friendship.GiftsThisWeek;
bool hasTalked = Game1.player.hasPlayerTalkedToNPC(name);
bool spouse = friendship.IsMarried();
bool housemate = spouse && SocialPage.isRoommateOfAnyone(name);
___kidsNames.Add("Robin");
___kidsNames.Add("Pierre");
___kidsNames.Add("Caroline");
___kidsNames.Add("Jodi");
___kidsNames.Add("Kent");
___kidsNames.Add("George");
___kidsNames.Add("Evelyn");
___kidsNames.Add("Demetrius");
string toSpeak = $"{name}";
if (!hasTalked)
{
toSpeak = $"{toSpeak}, not talked yet";
}
if (datable | housemate)
{
string text2 = (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.pt) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11635") : ((__instance.getGender(name) == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11635").Split('/').First() : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11635").Split('/').Last());
if (housemate)
{
text2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Housemate");
}
else if (spouse)
{
text2 = ((__instance.getGender(name) == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11636") : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11637"));
}
else if (__instance.isMarriedToAnyone(name))
{
text2 = ((__instance.getGender(name) == 0) ? Game1.content.LoadString("Strings\\UI:SocialPage_MarriedToOtherPlayer_MaleNPC") : Game1.content.LoadString("Strings\\UI:SocialPage_MarriedToOtherPlayer_FemaleNPC"));
}
else if (!Game1.player.isMarried() && friendship.IsDating())
{
text2 = ((__instance.getGender(name) == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11639") : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11640"));
}
else if (__instance.getFriendship(name).IsDivorced())
{
text2 = ((__instance.getGender(name) == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11642") : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11643"));
}
toSpeak = $"{toSpeak}, {text2}";
}
if (!__instance.getFriendship(name).IsMarried() && ___kidsNames.Contains(name))
{
toSpeak = $"{toSpeak}, married";
}
if (spouse)
{
toSpeak = $"{toSpeak}, spouse";
}
else if (friendship.IsDating())
{
toSpeak = $"{toSpeak}, dating";
}
toSpeak = $"{toSpeak}, {heartLevel} hearts, {giftsThisWeek} gifts given this week.";
if (socialPageQuery != toSpeak)
{
socialPageQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return true;
}
private static bool narrateFarmerDetails(SocialPage __instance, int i, List<ClickableTextureComponent> ___sprites, int x, int y)
{
long farmerID = (long)__instance.names[i];
Farmer farmer = Game1.getFarmerMaybeOffline(farmerID);
if (farmer == null)
return false;
int gender = (!farmer.IsMale) ? 1 : 0;
ClickableTextureComponent clickableTextureComponent = ___sprites[i];
if (!clickableTextureComponent.containsPoint(x, y))
return false;
Friendship friendship = Game1.player.team.GetFriendship(Game1.player.UniqueMultiplayerID, farmerID);
bool spouse = friendship.IsMarried();
string toSpeak = "";
string text2 = (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.pt) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11635") : ((gender == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11635").Split('/').First() : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11635").Split('/').Last());
if (spouse)
{
text2 = ((gender == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11636") : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11637"));
}
else if (farmer.isMarried() && !farmer.hasRoommate())
{
text2 = ((gender == 0) ? Game1.content.LoadString("Strings\\UI:SocialPage_MarriedToOtherPlayer_MaleNPC") : Game1.content.LoadString("Strings\\UI:SocialPage_MarriedToOtherPlayer_FemaleNPC"));
}
else if (!Game1.player.isMarried() && friendship.IsDating())
{
text2 = ((gender == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11639") : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11640"));
}
else if (friendship.IsDivorced())
{
text2 = ((gender == 0) ? Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11642") : Game1.content.LoadString("Strings\\StringsFromCSFiles:SocialPage.cs.11643"));
}
toSpeak = $"{farmer.displayName}, {text2}";
if (socialPageQuery != toSpeak)
{
socialPageQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return true;
}
internal static void Cleanup()
{
socialPageQuery = "";
}
}
}

View File

@@ -1,442 +0,0 @@
using Microsoft.Xna.Framework;
using stardew_access.Features;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class MenuPatches
{
private static string currentLetterText = " ";
private static string currentLevelUpTitle = " ";
public static Vector2? prevTile = null;
internal static bool PlaySoundPatch(string cueName)
{
try
{
if (!Context.IsPlayerFree)
return true;
if (!Game1.player.isMoving())
return true;
if (cueName == "grassyStep" || cueName == "sandyStep" || cueName == "snowyStep" || cueName == "stoneStep" || cueName == "thudStep" || cueName == "woodyStep")
{
Vector2 nextTile = CurrentPlayer.getNextTile();
if (ReadTile.isCollidingAtTile((int)nextTile.X, (int)nextTile.Y))
{
if (prevTile != nextTile)
{
prevTile = nextTile;
//Game1.playSound("colliding");
}
return false;
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
return true;
}
internal static void LanguageSelectionMenuPatch(LanguageSelectionMenu __instance)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY(); // Mouse x and y position
if (__instance.nextPageButton != null && __instance.nextPageButton.containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker($"Next Page Button", true);
return;
}
if (__instance.previousPageButton != null && __instance.previousPageButton.containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker($"Previous Page Button", true);
return;
}
for (int i = 0; i < __instance.languages.Count; i++)
{
if (__instance.languages[i].containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker($"{__instance.languageList[i]} Button", true);
break;
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void MineElevatorMenuPatch(List<ClickableComponent> ___elevators)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY(); // Mouse x and y position
for (int i = 0; i < ___elevators.Count; i++)
{
if (___elevators[i].containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker($"{___elevators[i].name} level", true);
break;
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void NamingMenuPatch(NamingMenu __instance, string title, TextBox ___textBox)
{
try
{
__instance.textBoxCC.snapMouseCursor();
___textBox.SelectMe();
string toSpeak = $"{title}";
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void ConfirmationDialogPatch(ConfirmationDialog __instance, string ___message)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY();
MainClass.GetScreenReader().SayWithMenuChecker(___message, true);
if (__instance.okButton.containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker("Ok Button", false);
}
else if (__instance.cancelButton.containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker("Cancel Button", false);
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void LevelUpMenuPatch(LevelUpMenu __instance, List<int> ___professionsToChoose, List<string> ___leftProfessionDescription, List<string> ___rightProfessionDescription, List<string> ___extraInfoForLevel, List<CraftingRecipe> ___newCraftingRecipes, string ___title, bool ___isActive, bool ___isProfessionChooser)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY();
string leftProfession = " ", rightProfession = " ", extraInfo = " ", newCraftingRecipe = " ", toSpeak = " ";
bool isOpenBracketPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.OemOpenBrackets); // for left click
bool isLeftCtrlPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftControl);
bool isEnterPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Enter);
if (!__instance.informationUp)
{
return;
}
if (__instance.isProfessionChooser)
{
if (___professionsToChoose.Count() == 0)
{
return;
}
for (int j = 0; j < ___leftProfessionDescription.Count; j++)
{
leftProfession += ___leftProfessionDescription[j] + ", ";
}
for (int i = 0; i < ___rightProfessionDescription.Count; i++)
{
rightProfession += ___rightProfessionDescription[i] + ", ";
}
if (__instance.leftProfession.containsPoint(x, y))
{
if (isOpenBracketPressed || (isLeftCtrlPressed && isEnterPressed && __instance.readyToClose()))
{
Game1.player.professions.Add(___professionsToChoose[0]);
__instance.getImmediateProfessionPerk(___professionsToChoose[0]);
___isActive = false;
__instance.informationUp = false;
___isProfessionChooser = false;
__instance.RemoveLevelFromLevelList();
__instance.exitThisMenu();
return;
}
toSpeak = $"Selected: {leftProfession} Left click to choose.";
}
if (__instance.rightProfession.containsPoint(x, y))
{
if (isOpenBracketPressed || (isLeftCtrlPressed && isEnterPressed && __instance.readyToClose()))
{
Game1.player.professions.Add(___professionsToChoose[1]);
__instance.getImmediateProfessionPerk(___professionsToChoose[1]);
___isActive = false;
__instance.informationUp = false;
___isProfessionChooser = false;
__instance.RemoveLevelFromLevelList();
__instance.exitThisMenu();
return;
}
toSpeak = $"Selected: {rightProfession} Left click to choose.";
}
}
else
{
foreach (string s2 in ___extraInfoForLevel)
{
extraInfo += s2 + ", ";
}
foreach (CraftingRecipe s in ___newCraftingRecipes)
{
string cookingOrCrafting = Game1.content.LoadString("Strings\\UI:LearnedRecipe_" + (s.isCookingRecipe ? "cooking" : "crafting"));
string message = Game1.content.LoadString("Strings\\UI:LevelUp_NewRecipe", cookingOrCrafting, s.DisplayName);
newCraftingRecipe += $"{message}, ";
}
}
if (__instance.okButton.containsPoint(x, y))
{
if (isOpenBracketPressed || (isLeftCtrlPressed && isEnterPressed))
__instance.okButtonClicked();
toSpeak = $"{___title} {extraInfo} {newCraftingRecipe}. Left click to close.";
}
if (toSpeak != " ")
MainClass.GetScreenReader().SayWithMenuChecker(toSpeak, true);
else if (__instance.isProfessionChooser && currentLevelUpTitle != $"{___title}. Select a new profession.")
{
MainClass.GetScreenReader().SayWithMenuChecker($"{___title}. Select a new profession.", true);
currentLevelUpTitle = $"{___title}. Select a new profession.";
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void ShippingMenuPatch(ShippingMenu __instance, List<int> ___categoryTotals)
{
try
{
bool isLeftControlPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftControl);
bool isOpenBracketPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.OemOpenBrackets); // for left click
bool isEnterPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Enter);
if (__instance.currentPage == -1)
{
int total = ___categoryTotals[5];
string toSpeak;
if (__instance.okButton.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
// Perform Left Click
if (isOpenBracketPressed || (isLeftControlPressed && isEnterPressed))
{
Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true));
}
toSpeak = $"{total}g in total. Press left mouse button to save.";
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
for (int i = 0; i < __instance.categories.Count; i++)
{
if (__instance.categories[i].containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
toSpeak = $"Money recieved from {__instance.getCategoryName(i)}: {___categoryTotals[i]}g.";
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void LetterViewerMenuPatch(LetterViewerMenu __instance)
{
try
{
if (!__instance.IsActive())
return;
#region Texts in the letter
string message = __instance.mailMessage[__instance.page];
string toSpeak = $"{message}";
if (__instance.ShouldShowInteractable())
{
if (__instance.moneyIncluded > 0)
{
string moneyText = Game1.content.LoadString("Strings\\UI:LetterViewer_MoneyIncluded", __instance.moneyIncluded);
toSpeak += $"\t\n\t ,Included money: {moneyText}";
}
else if (__instance.learnedRecipe != null && __instance.learnedRecipe.Length > 0)
{
string recipeText = Game1.content.LoadString("Strings\\UI:LetterViewer_LearnedRecipe", __instance.cookingOrCrafting);
toSpeak += $"\t\n\t ,Learned Recipe: {recipeText}";
}
}
if (currentLetterText != toSpeak)
{
currentLetterText = toSpeak;
// snap mouse to accept quest button
if (__instance.acceptQuestButton != null && __instance.acceptQuestButton.visible)
{
toSpeak += "\t\n Left click to accept quest.";
__instance.acceptQuestButton.snapMouseCursorToCenter();
}
MainClass.GetScreenReader().Say(toSpeak, false);
}
#endregion
#region Narrate items given in the mail
if (__instance.ShouldShowInteractable())
{
foreach (ClickableComponent c in __instance.itemsToGrab)
{
string name = c.name;
string label = c.label;
if (c.containsPoint(Game1.getMousePosition().X, Game1.getMousePosition().Y))
MainClass.GetScreenReader().SayWithChecker($"Grab: {name} \t\n {label}", false);
}
}
#endregion
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void Game1ExitActiveMenuPatch()
{
try
{
if (Game1.activeClickableMenu is GameMenu)
{
GameMenuPatches.gameMenuQueryKey = "";
GameMenuPatches.craftingPageQueryKey = "";
GameMenuPatches.inventoryPageQueryKey = "";
GameMenuPatches.exitPageQueryKey = "";
GameMenuPatches.optionsPageQueryKey = "";
GameMenuPatches.socialPageQuery = "";
GameMenuPatches.currentSelectedCraftingRecipe = -1;
GameMenuPatches.isSelectingRecipe = false;
}
if (Game1.activeClickableMenu is JunimoNoteMenu)
{
GameMenuPatches.currentIngredientListItem = -1;
GameMenuPatches.currentIngredientInputSlot = -1;
GameMenuPatches.currentInventorySlot = -1;
GameMenuPatches.junimoNoteMenuQuery = "";
}
if (Game1.activeClickableMenu is ShopMenu)
{
GameMenuPatches.shopMenuQueryKey = "";
}
if (Game1.activeClickableMenu is ItemGrabMenu)
{
GameMenuPatches.itemGrabMenuQueryKey = "";
}
GameMenuPatches.hoveredItemQueryKey = "";
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void IClickableMenuOnExitPatch(IClickableMenu __instance)
{
try
{
if (__instance is GeodeMenu)
{
GameMenuPatches.geodeMenuQueryKey = "";
}
if (__instance is ItemGrabMenu)
{
GameMenuPatches.itemGrabMenuQueryKey = "";
}
if (__instance is ShopMenu)
{
GameMenuPatches.shopMenuQueryKey = "";
}
if (__instance is CarpenterMenu)
{
BuildingNAnimalMenuPatches.carpenterMenuQuery = "";
BuildingNAnimalMenuPatches.isUpgrading = false;
BuildingNAnimalMenuPatches.isDemolishing = false;
BuildingNAnimalMenuPatches.isPainting = false;
BuildingNAnimalMenuPatches.isMoving = false;
BuildingNAnimalMenuPatches.isConstructing = false;
BuildingNAnimalMenuPatches.carpenterMenu = null;
}
if (__instance is PurchaseAnimalsMenu)
{
BuildingNAnimalMenuPatches.purchaseAnimalMenuQuery = "";
BuildingNAnimalMenuPatches.firstTimeInNamingMenu = true;
BuildingNAnimalMenuPatches.purchaseAnimalsMenu = null;
}
if (__instance is DialogueBox)
{
DialoguePatches.isDialogueAppearingFirstTime = true;
DialoguePatches.currentDialogue = " ";
}
GameMenuPatches.hoveredItemQueryKey = "";
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void ExitEventPatch()
{
if (MainClass.GetScreenReader() != null)
MainClass.GetScreenReader().CloseScreenReader();
}
internal static void resetGlobalVars()
{
currentLetterText = " ";
currentLevelUpTitle = " ";
}
}
}

View File

@@ -0,0 +1,116 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ForgeMenuPatch
{
private static string forgeMenuQuery = "";
internal static void DrawPatch(ForgeMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (narrateHoveredButton(__instance, x, y)) return;
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y))
{
Cleanup();
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"An error occured in forge menu patch:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateHoveredButton(ForgeMenu __instance, int x, int y)
{
string toSpeak = "";
bool isDropItemButton = false;
if (__instance.leftIngredientSpot != null && __instance.leftIngredientSpot.containsPoint(x, y))
{
if (__instance.leftIngredientSpot.item == null)
{
toSpeak = "Input weapon or tool here";
}
else
{
Item item = __instance.leftIngredientSpot.item;
toSpeak = $"Weapon slot: {item.Stack} {item.DisplayName}";
}
}
else if (__instance.rightIngredientSpot != null && __instance.rightIngredientSpot.containsPoint(x, y))
{
if (__instance.rightIngredientSpot.item == null)
{
toSpeak = "Input gemstone here";
}
else
{
Item item = __instance.rightIngredientSpot.item;
toSpeak = $"Gemstone slot: {item.Stack} {item.DisplayName}";
}
}
else if (__instance.startTailoringButton != null && __instance.startTailoringButton.containsPoint(x, y))
{
toSpeak = "Star forging button";
}
else if (__instance.unforgeButton != null && __instance.unforgeButton.containsPoint(x, y))
{
toSpeak = "Unforge button";
}
else if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trashcan";
}
else if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "ok button";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "drop item";
isDropItemButton = true;
}
else if (__instance.equipmentIcons.Count > 0 && __instance.equipmentIcons[0].containsPoint(x, y))
{
toSpeak = "Left ring Slot";
if (Game1.player.leftRing.Value != null)
toSpeak = $"{toSpeak}: {Game1.player.leftRing.Value.DisplayName}";
}
else if (__instance.equipmentIcons.Count > 0 && __instance.equipmentIcons[1].containsPoint(x, y))
{
toSpeak = "Right ring Slot";
if (Game1.player.rightRing.Value != null)
toSpeak = $"{toSpeak}: {Game1.player.rightRing.Value.DisplayName}";
}
else
{
return false;
}
if (forgeMenuQuery != toSpeak)
{
forgeMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
}
return true;
}
internal static void Cleanup()
{
forgeMenuQuery = "";
}
}
}

View File

@@ -0,0 +1,88 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class GeodeMenuPatch
{
private static string geodeMenuQueryKey = "";
internal static void DrawPatch(GeodeMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (narrateRecievedTreasure(__instance)) return;
if (narrateHoveredButton(__instance, x, y)) return;
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y))
geodeMenuQueryKey = "";
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateRecievedTreasure(GeodeMenu __instance)
{
// Narrates the treasure recieved on breaking the geode
if (__instance.geodeTreasure == null) return false;
string name = __instance.geodeTreasure.DisplayName;
int stack = __instance.geodeTreasure.Stack;
string toSpeak = $"Recieved {stack} {name}";
if (geodeMenuQueryKey != toSpeak)
{
geodeMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return true;
}
private static bool narrateHoveredButton(GeodeMenu __instance, int x, int y)
{
string toSpeak = "";
bool isDropItemButton = false;
if (__instance.geodeSpot != null && __instance.geodeSpot.containsPoint(x, y))
{
toSpeak = "Place geode here";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "Drop item here";
isDropItemButton = true;
}
else if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trash can";
}
else if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "Ok button";
}
else
{
return false;
}
if (geodeMenuQueryKey == toSpeak) return true;
geodeMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
return true;
}
internal static void Cleanup()
{
geodeMenuQueryKey = "";
}
}
}

View File

@@ -0,0 +1,230 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ItemGrabMenuPatch
{
internal static string itemGrabMenuQueryKey = "";
internal static string hoveredItemQueryKey = "";
internal static void DrawPatch(ItemGrabMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (MainClass.Config.SnapToFirstSecondaryInventorySlotKey.JustPressed() && __instance.ItemsToGrabMenu.inventory.Count > 0 && !__instance.shippingBin)
{
__instance.setCurrentlySnappedComponentTo(__instance.ItemsToGrabMenu.inventory[0].myID);
__instance.ItemsToGrabMenu.inventory[0].snapMouseCursorToCenter();
}
else if (MainClass.Config.SnapToFirstInventorySlotKey.JustPressed() && __instance.inventory.inventory.Count > 0)
{
__instance.setCurrentlySnappedComponentTo(__instance.inventory.inventory[0].myID);
__instance.inventory.inventory[0].snapMouseCursorToCenter();
}
if (narrateHoveredButton(__instance, x, y))
{
InventoryUtils.Cleanup();
return;
}
if (narrateLastShippedItem(__instance, x, y))
{
InventoryUtils.Cleanup();
return;
}
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y, true))
{
itemGrabMenuQueryKey = "";
return;
}
if (InventoryUtils.narrateHoveredSlot(__instance.ItemsToGrabMenu, __instance.ItemsToGrabMenu.inventory, __instance.ItemsToGrabMenu.actualInventory, x, y, true))
{
itemGrabMenuQueryKey = "";
return;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateHoveredButton(ItemGrabMenu __instance, int x, int y)
{
string toSpeak = "";
bool isDropItemButton = false;
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "Ok Button";
}
else if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trash Can";
}
else if (__instance.organizeButton != null && __instance.organizeButton.containsPoint(x, y))
{
toSpeak = "Organize Button";
}
else if (__instance.fillStacksButton != null && __instance.fillStacksButton.containsPoint(x, y))
{
toSpeak = "Add to existing stacks button";
}
else if (__instance.specialButton != null && __instance.specialButton.containsPoint(x, y))
{
toSpeak = "Special Button";
}
else if (__instance.colorPickerToggleButton != null && __instance.colorPickerToggleButton.containsPoint(x, y))
{
toSpeak = "Color Picker: " + (__instance.chestColorPicker.visible ? "Enabled" : "Disabled");
}
else if (__instance.junimoNoteIcon != null && __instance.junimoNoteIcon.containsPoint(x, y))
{
toSpeak = "Community Center Button";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "Drop Item";
isDropItemButton = true;
}
else
{
return false;
}
// FIXME
/*if (__instance.discreteColorPickerCC.Count > 0) {
for (int i = 0; i < __instance.discreteColorPickerCC.Count; i++)
{
if (__instance.discreteColorPickerCC[i].containsPoint(x, y))
{
MainClass.monitor.Log(i.ToString(), LogLevel.Debug);
string toSpeak = getChestColorName(i);
if (itemGrabMenuQueryKey != toSpeak)
{
itemGrabMenuQueryKey = toSpeak;
hoveredItemQueryKey = "";
ScreenReader.say(toSpeak, true);
Game1.playSound("sa_drop_item");
}
return;
}
}
}*/
if (itemGrabMenuQueryKey == toSpeak) return true;
itemGrabMenuQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
return true;
}
private static bool narrateLastShippedItem(ItemGrabMenu __instance, int x, int y)
{
if (!__instance.shippingBin || Game1.getFarm().lastItemShipped == null || !__instance.lastShippedHolder.containsPoint(x, y))
return false;
Item lastShippedItem = Game1.getFarm().lastItemShipped;
string name = lastShippedItem.DisplayName;
int count = lastShippedItem.Stack;
string toSpeak = $"Last Shipped: {count} {name}";
if (itemGrabMenuQueryKey != toSpeak)
{
itemGrabMenuQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
}
return true;
}
// TODO Add color names
private static string getChestColorName(int i)
{
string toReturn = "";
switch (i)
{
case 0:
toReturn = "Default chest color";
break;
case 1:
toReturn = "Default chest color";
break;
case 2:
toReturn = "Default chest color";
break;
case 3:
toReturn = "Default chest color";
break;
case 4:
toReturn = "Default chest color";
break;
case 5:
toReturn = "Default chest color";
break;
case 6:
toReturn = "Default chest color";
break;
case 7:
toReturn = "Default chest color";
break;
case 8:
toReturn = "Default chest color";
break;
case 9:
toReturn = "Default chest color";
break;
case 10:
toReturn = "Default chest color";
break;
case 11:
toReturn = "Default chest color";
break;
case 12:
toReturn = "Default chest color";
break;
case 13:
toReturn = "Default chest color";
break;
case 14:
toReturn = "Default chest color";
break;
case 15:
toReturn = "Default chest color";
break;
case 16:
toReturn = "Default chest color";
break;
case 17:
toReturn = "Default chest color";
break;
case 18:
toReturn = "Default chest color";
break;
case 19:
toReturn = "Default chest color";
break;
case 20:
toReturn = "Default chest color";
break;
}
return toReturn;
}
internal static void Cleanup()
{
hoveredItemQueryKey = "";
itemGrabMenuQueryKey = "";
}
}
}

View File

@@ -0,0 +1,121 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ShopMenuPatch
{
internal static string shopMenuQueryKey = "";
internal static string hoveredItemQueryKey = "";
internal static void DrawPatch(ShopMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (MainClass.Config.SnapToFirstSecondaryInventorySlotKey.JustPressed() && __instance.forSaleButtons.Count > 0)
{
__instance.forSaleButtons[0].snapMouseCursorToCenter();
__instance.setCurrentlySnappedComponentTo(__instance.forSaleButtons[0].myID);
}
else if (MainClass.Config.SnapToFirstInventorySlotKey.JustPressed() && __instance.inventory.inventory.Count > 0)
{
__instance.inventory.inventory[0].snapMouseCursorToCenter();
__instance.setCurrentlySnappedComponentTo(__instance.inventory.inventory[0].myID);
}
if (narrateHoveredButton(__instance, x, y)) return;
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y, hoverPrice: __instance.hoverPrice))
{
shopMenuQueryKey = "";
return;
}
narrateHoveredSellingItem(__instance);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateHoveredButton(ShopMenu __instance, int x, int y)
{
string toSpeak = "";
bool isDropItemButton = false;
if (__instance.inventory.dropItemInvisibleButton != null && __instance.inventory.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "Drop Item";
isDropItemButton = true;
}
else if (__instance.upArrow != null && __instance.upArrow.containsPoint(x, y))
{
toSpeak = "Up Arrow Button";
}
else if (__instance.downArrow != null && __instance.downArrow.containsPoint(x, y))
{
toSpeak = "Down Arrow Button";
}
else
{
return false;
}
if (shopMenuQueryKey == toSpeak) return true;
shopMenuQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
return true;
}
private static void narrateHoveredSellingItem(ShopMenu __instance)
{
if (__instance.hoveredItem == null) return;
string name = __instance.hoveredItem.DisplayName;
string price = $"Buy Price: {__instance.hoverPrice} g";
string description = __instance.hoveredItem.getDescription();
string requirements = "";
#region get required items for item
int itemIndex = -1, itemAmount = 5;
if (__instance.itemPriceAndStock[__instance.hoveredItem].Length > 2)
itemIndex = __instance.itemPriceAndStock[__instance.hoveredItem][2];
if (__instance.itemPriceAndStock[__instance.hoveredItem].Length > 3)
itemAmount = __instance.itemPriceAndStock[__instance.hoveredItem][3];
if (itemIndex != -1)
{
string itemName = Game1.objectInformation[itemIndex].Split('/')[0];
if (itemAmount != -1)
requirements = $"Required: {itemAmount} {itemName}";
else
requirements = $"Required: {itemName}";
}
#endregion
string toSpeak = $"{name}, {requirements}, {price}, \n\t{description}";
if (shopMenuQueryKey == toSpeak) return;
shopMenuQueryKey = toSpeak;
hoveredItemQueryKey = "";
MainClass.ScreenReader.Say(toSpeak, true);
}
internal static void Cleanup()
{
shopMenuQueryKey = "";
hoveredItemQueryKey = "";
}
}
}

View File

@@ -0,0 +1,115 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class TailoringMenuPatch
{
internal static string tailoringMenuQuery = "";
internal static void DrawPatch(TailoringMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (InventoryUtils.narrateHoveredSlot(__instance.inventory, __instance.inventory.inventory, __instance.inventory.actualInventory, x, y))
return;
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static bool narrateHoveredButton(TailoringMenu __instance, int x, int y)
{
string toSpeak = "";
bool isDropItemButton = false;
if (__instance.leftIngredientSpot != null && __instance.leftIngredientSpot.containsPoint(x, y))
{
if (__instance.leftIngredientSpot.item == null)
{
toSpeak = "Input cloth here";
}
else
{
Item item = __instance.leftIngredientSpot.item;
toSpeak = $"Cloth slot: {item.Stack} {item.DisplayName}";
}
}
else if (__instance.rightIngredientSpot != null && __instance.rightIngredientSpot.containsPoint(x, y))
{
if (__instance.rightIngredientSpot.item == null)
{
toSpeak = "Input ingredient here";
}
else
{
Item item = __instance.rightIngredientSpot.item;
toSpeak = $"Ingredient slot: {item.Stack} {item.DisplayName}";
}
}
else if (__instance.startTailoringButton != null && __instance.startTailoringButton.containsPoint(x, y))
{
toSpeak = "Star tailoring button";
}
else if (__instance.trashCan != null && __instance.trashCan.containsPoint(x, y))
{
toSpeak = "Trashcan";
}
else if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "ok button";
}
else if (__instance.dropItemInvisibleButton != null && __instance.dropItemInvisibleButton.containsPoint(x, y))
{
toSpeak = "drop item";
isDropItemButton = true;
}
else if (__instance.equipmentIcons.Count > 0 && __instance.equipmentIcons[0].containsPoint(x, y))
{
toSpeak = "Hat Slot";
if (Game1.player.hat.Value != null)
toSpeak = $"{toSpeak}: {Game1.player.hat.Value.DisplayName}";
}
else if (__instance.equipmentIcons.Count > 0 && __instance.equipmentIcons[1].containsPoint(x, y))
{
toSpeak = "Shirt Slot";
if (Game1.player.shirtItem.Value != null)
toSpeak = $"{toSpeak}: {Game1.player.shirtItem.Value.DisplayName}";
}
else if (__instance.equipmentIcons.Count > 0 && __instance.equipmentIcons[2].containsPoint(x, y))
{
toSpeak = "Pants Slot";
if (Game1.player.pantsItem.Value != null)
toSpeak = $"{toSpeak}: {Game1.player.pantsItem.Value.DisplayName}";
}
else {
return false;
}
if (tailoringMenuQuery != toSpeak)
{
tailoringMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
if (isDropItemButton) Game1.playSound("drop_item");
}
return true;
}
internal static void Cleanup()
{
tailoringMenuQuery = "";
}
}
}

View File

@@ -0,0 +1,88 @@
using Microsoft.Xna.Framework;
using StardewValley;
using StardewValley.Minigames;
namespace stardew_access.Patches
{
public class GrandpaStoryPatch
{
public static string grandpaStoryQuery = " ";
internal static void DrawPatch(GrandpaStory __instance, StardewValley.Menus.LetterViewerMenu ___letterView, bool ___drawGrandpa, bool ___letterReceived, bool ___mouseActive, Queue<string> ___grandpaSpeech, int ___grandpaSpeechTimer, int ___totalMilliseconds, int ___scene, int ___parallaxPan)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
string toSpeak = " ";
if (___letterView != null)
{
LetterViwerMenuPatch.narrateLetterContent(___letterView);
}
if (MainClass.ModHelper == null)
return;
if (___scene == 0)
{
toSpeak = MainClass.ModHelper.Translation.Get("grandpastory.scene0");
}
else if (___drawGrandpa)
{
if (___grandpaSpeech.Count > 0 && ___grandpaSpeechTimer > 3000)
{
toSpeak = ___grandpaSpeech.Peek();
}
}
else if (___scene == 3)
{
toSpeak = Game1.content.LoadString("Strings\\StringsFromCSFiles:GrandpaStory.cs.12059");
}
else if (___scene == 4)
{
toSpeak = MainClass.ModHelper.Translation.Get("grandpastory.scene4");
}
else if (___scene == 5)
{
toSpeak = MainClass.ModHelper.Translation.Get("grandpastory.scene5");
}
else if (___scene == 6)
{
if (___grandpaSpeechTimer > 3000)
{
if (clickableGrandpaLetterRect(___parallaxPan, ___grandpaSpeechTimer).Contains(x, y))
{
toSpeak = MainClass.ModHelper.Translation.Get("grandpastory.letteropen");
}
else if (___letterView == null)
{
Point pos = clickableGrandpaLetterRect(___parallaxPan, ___grandpaSpeechTimer).Center;
Game1.setMousePositionRaw((int)((float)pos.X * Game1.options.zoomLevel), (int)((float)pos.Y * Game1.options.zoomLevel));
return;
}
}
else
{
toSpeak = MainClass.ModHelper.Translation.Get("grandpastory.scene6");
}
}
if (toSpeak != " " && grandpaStoryQuery != toSpeak)
{
grandpaStoryQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, false);
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
// This method is taken from the game's source code
private static Rectangle clickableGrandpaLetterRect(int ___parallaxPan, int ___grandpaSpeechTimer)
{
return new Rectangle((int)Utility.getTopLeftPositionForCenteringOnScreen(Game1.viewport, 1294, 730).X + (286 - ___parallaxPan) * 4, (int)Utility.getTopLeftPositionForCenteringOnScreen(Game1.viewport, 1294, 730).Y + 218 + Math.Max(0, Math.Min(60, (___grandpaSpeechTimer - 5000) / 8)), 524, 344);
}
}
}

View File

@@ -0,0 +1,40 @@
using StardewValley.Minigames;
namespace stardew_access.Patches
{
public class IntroPatch
{
public static string introQuery = " ";
internal static void DrawPatch(Intro __instance, int ___currentState)
{
try
{
if (MainClass.ModHelper == null)
return;
string toSpeak = " ";
if (___currentState == 3)
{
toSpeak = MainClass.ModHelper.Translation.Get("intro.scene3");
}
else if (___currentState == 4)
{
toSpeak = MainClass.ModHelper.Translation.Get("intro.scene4");
}
if (toSpeak != " " && introQuery != toSpeak)
{
introQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, false);
return;
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"An error occured in intro minigame patch:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,70 @@
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ChatBoxPatch
{
private static int currentChatMessageIndex = 0;
private static bool isChatRunning = false;
internal static void UpdatePatch(ChatBox __instance, List<ChatMessage> ___messages)
{
try
{
string toSpeak = " ";
if (__instance.chatBox.Selected)
{
bool isPrevButtonPressed = MainClass.Config.ChatMenuNextKey.JustPressed();
bool isNextButtonPressed = MainClass.Config.ChatMenuPreviousKey.JustPressed();
if (___messages.Count <= 0) return;
#region To narrate previous and next chat messages
if (isNextButtonPressed && !isChatRunning)
{
isChatRunning = true;
CycleThroughChatMessages(true, ___messages);
Task.Delay(200).ContinueWith(_ => { isChatRunning = false; });
}
else if (isPrevButtonPressed && !isChatRunning)
{
isChatRunning = true;
CycleThroughChatMessages(false, ___messages);
Task.Delay(200).ContinueWith(_ => { isChatRunning = false; });
}
#endregion
}
else if (___messages.Count > 0)
{
#region To narrate latest chat message
___messages[___messages.Count - 1].message.ForEach(message =>
{
toSpeak += $"{message.message}, ";
});
if (toSpeak != " ")
MainClass.ScreenReader.SayWithChatChecker(toSpeak, false);
#endregion
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static void CycleThroughChatMessages(bool increase, List<ChatMessage> ___messages)
{
string toSpeak = " ";
currentChatMessageIndex = (increase) ? (Math.Min(currentChatMessageIndex + 1, ___messages.Count - 1)) : (currentChatMessageIndex = Math.Max(currentChatMessageIndex - 1, 0));
___messages[currentChatMessageIndex].message.ForEach(message =>
{
toSpeak += $"{message.message}, ";
});
MainClass.ScreenReader.Say(toSpeak, true);
}
}
}

View File

@@ -0,0 +1,128 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class DialogueBoxPatch
{
private static string currentDialogue = "";
private static bool isDialogueAppearingFirstTime = true;
internal static void DrawPatch(DialogueBox __instance)
{
try
{
if (__instance.transitioning) return;
if (narrateCharacterDialogue(__instance)) return;
if (narrateQuestionDialogue(__instance)) return;
narrateBasicDialogue(__instance.getCurrentString());
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}");
}
}
internal static void RecieveLeftClickPatch()
{
// CLears the currentDialogue string on closing dialog
Cleanup();
}
private static bool narrateCharacterDialogue(DialogueBox __instance)
{
if (__instance.characterDialogue == null) return false;
// For Normal Character dialogues
Dialogue dialogue = __instance.characterDialogue;
string speakerName = dialogue.speaker.displayName;
string dialogueText = "";
string responseText = "";
bool hasResponses = dialogue.isCurrentDialogueAQuestion();
dialogueText = $"{speakerName} said {__instance.getCurrentString()}";
if (hasResponses)
{
responseText = getCurrentResponseText(__instance);
CheckAndSpeak(isDialogueAppearingFirstTime ? $"{dialogueText} \n\t {responseText}" : responseText, responseText);
if (isDialogueAppearingFirstTime) isDialogueAppearingFirstTime = false;
}
else
{
CheckAndSpeak(dialogueText);
}
return true;
}
private static bool narrateQuestionDialogue(DialogueBox __instance)
{
if (!__instance.isQuestion) return false;
// For Dialogues with responses/answers like the dialogue when we click on tv
string questionText = "";
string responseText = "";
bool hasResponses = false;
if (__instance.responses.Count > 0) hasResponses = true;
if (!hasResponses) return false;
questionText = __instance.getCurrentString();
responseText = getCurrentResponseText(__instance);
CheckAndSpeak(isDialogueAppearingFirstTime ? $"{questionText} \n\t {responseText}" : responseText, responseText);
if (isDialogueAppearingFirstTime) isDialogueAppearingFirstTime = false;
return true;
}
private static void narrateBasicDialogue(string dialogue)
{
// Basic dialogues like `No mails in the mail box`
if (Game1.activeClickableMenu is not DialogueBox) return;
CheckAndSpeak(dialogue);
}
private static string getCurrentResponseText(DialogueBox __instance)
{
List<Response> responses = __instance.responses;
if (__instance.selectedResponse >= 0 && __instance.selectedResponse < responses.Count)
{
return $"{__instance.selectedResponse + 1}: {responses[__instance.selectedResponse].responseText}";
}
else
{
// When the dialogue is not finished writing then the selectedResponse is <0 and this results
// in the first response not being detcted, so this sets the first response option to be the default
// if the current dialogue is a question or has responses
return $"1: {responses[0].responseText}";
}
}
private static void CheckAndSpeak(string toSpeak)
{
if (currentDialogue == toSpeak) return;
currentDialogue = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
private static void CheckAndSpeak(string toSpeak, string checkQuery)
{
if (currentDialogue == checkQuery) return;
currentDialogue = checkQuery;
MainClass.ScreenReader.Say(toSpeak, true);
}
internal static void Cleanup()
{
currentDialogue = "";
isDialogueAppearingFirstTime = true;
}
}
}

View File

@@ -0,0 +1,62 @@
using Microsoft.Xna.Framework;
using stardew_access.Features;
using StardewModdingAPI;
using StardewValley;
namespace stardew_access.Patches
{
internal class Game1Patch
{
private static Vector2? prevTile = null;
internal static void ExitActiveMenuPatch()
{
try
{
MainClass.DebugLog($"Closing {Game1.activeClickableMenu.GetType().ToString()} menu, performing cleanup...");
IClickableMenuPatch.Cleanup(Game1.activeClickableMenu);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void CloseTextEntryPatch()
{
TextBoxPatch.activeTextBoxes = "";
}
internal static bool PlaySoundPatch(string cueName)
{
try
{
if (!Context.IsPlayerFree)
return true;
if (!Game1.player.isMoving())
return true;
if (cueName == "grassyStep" || cueName == "sandyStep" || cueName == "snowyStep" || cueName == "stoneStep" || cueName == "thudStep" || cueName == "woodyStep")
{
Vector2 nextTile = CurrentPlayer.FacingTile;
if (TileInfo.IsCollidingAtTile(Game1.currentLocation, (int)nextTile.X, (int)nextTile.Y))
{
if (prevTile != nextTile)
{
prevTile = nextTile;
//Game1.playSound("colliding");
}
return false;
}
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
return true;
}
}
}

View File

@@ -0,0 +1,301 @@
using stardew_access.Features;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
// These patches are global, i.e. work on every menus
internal class IClickableMenuPatch
{
internal static void DrawHoverTextPatch(string? text, int moneyAmountToDisplayAtBottom = -1, string? boldTitleText = null, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, string[]? buffIconsToDisplay = null, Item? hoveredItem = null, CraftingRecipe? craftingIngredients = null)
{
try
{
#region Skip narrating hover text for certain menus
if (Game1.activeClickableMenu is TitleMenu && !(((TitleMenu)Game1.activeClickableMenu).GetChildMenu() is CharacterCustomization))
return;
else if (Game1.activeClickableMenu is LetterViewerMenu || Game1.activeClickableMenu is QuestLog)
return;
else if (Game1.activeClickableMenu is Billboard)
return;
else if (Game1.activeClickableMenu is GeodeMenu)
return;
else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is InventoryPage)
return;
else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is CraftingPage)
return;
else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is OptionsPage)
return;
else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is ExitPage)
return;
else if (Game1.activeClickableMenu is GameMenu && ((GameMenu)Game1.activeClickableMenu).GetCurrentPage() is SocialPage)
return;
else if (Game1.activeClickableMenu is ItemGrabMenu)
return;
else if (Game1.activeClickableMenu is ShopMenu)
return;
else if (Game1.activeClickableMenu is ConfirmationDialog)
return;
else if (Game1.activeClickableMenu is JunimoNoteMenu)
return;
else if (Game1.activeClickableMenu is CarpenterMenu)
return;
else if (Game1.activeClickableMenu is PurchaseAnimalsMenu)
return;
else if (Game1.activeClickableMenu is CraftingPage)
return;
else if (Game1.activeClickableMenu is AnimalQueryMenu)
return;
else if (Game1.activeClickableMenu is ConfirmationDialog)
return;
else if (Game1.activeClickableMenu is ReadyCheckDialog)
return;
else if (Game1.activeClickableMenu is JojaCDMenu)
return;
else if (Game1.activeClickableMenu is TailoringMenu)
return;
else if (Game1.activeClickableMenu is PondQueryMenu)
return;
else if (Game1.activeClickableMenu is ForgeMenu)
return;
else if (Game1.activeClickableMenu is ItemListMenu)
return;
else if (Game1.activeClickableMenu is FieldOfficeMenu)
return;
else if (Game1.activeClickableMenu is MuseumMenu)
return;
#endregion
string toSpeak = " ";
#region Add item count before title
if (hoveredItem != null && hoveredItem.HasBeenInInventory)
{
int count = hoveredItem.Stack;
if (count > 1)
toSpeak = $"{toSpeak} {count} ";
}
#endregion
#region Add title if any
if (boldTitleText != null)
toSpeak = $"{toSpeak} {boldTitleText}\n";
#endregion
#region Add quality of item
if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Quality > 0)
{
int quality = ((StardewValley.Object)hoveredItem).Quality;
if (quality == 1)
{
toSpeak = $"{toSpeak} Silver quality";
}
else if (quality == 2 || quality == 3)
{
toSpeak = $"{toSpeak} Gold quality";
}
else if (quality >= 4)
{
toSpeak = $"{toSpeak} Iridium quality";
}
}
#endregion
#region Narrate hovered required ingredients
if (extraItemToShowIndex != -1)
{
string itemName = Game1.objectInformation[extraItemToShowIndex].Split('/')[0];
if (extraItemToShowAmount != -1)
toSpeak = $"{toSpeak} Required: {extraItemToShowAmount} {itemName}";
else
toSpeak = $"{toSpeak} Required: {itemName}";
}
#endregion
#region Add money
if (moneyAmountToDisplayAtBottom != -1)
toSpeak = $"{toSpeak} \nCost: {moneyAmountToDisplayAtBottom}g\n";
#endregion
#region Add the base text
if (text == "???")
toSpeak = "unknown";
else
toSpeak = $"{toSpeak} {text}";
#endregion
#region Add crafting ingredients
if (craftingIngredients != null)
{
toSpeak = $"{toSpeak} \n{craftingIngredients.description}";
toSpeak = $"{toSpeak} \nIngredients\n";
craftingIngredients.recipeList.ToList().ForEach(recipe =>
{
int count = recipe.Value;
int item = recipe.Key;
string name = craftingIngredients.getNameFromIndex(item);
toSpeak = $"{toSpeak} ,{count} {name}";
});
}
#endregion
#region Add health & stamina
if (hoveredItem is StardewValley.Object && ((StardewValley.Object)hoveredItem).Edibility != -300)
{
int stamina_recovery = ((StardewValley.Object)hoveredItem).staminaRecoveredOnConsumption();
toSpeak = $"{toSpeak} {stamina_recovery} Energy\n";
if (stamina_recovery >= 0)
{
int health_recovery = ((StardewValley.Object)hoveredItem).healthRecoveredOnConsumption();
toSpeak = $"{toSpeak} {health_recovery} Health";
}
}
#endregion
#region Add buff items (effects like +1 walking speed)
if (buffIconsToDisplay != null)
{
for (int i = 0; i < buffIconsToDisplay.Length; i++)
{
string buffName = ((Convert.ToInt32(buffIconsToDisplay[i]) > 0) ? "+" : "") + buffIconsToDisplay[i] + " ";
if (i <= 11)
{
buffName = Game1.content.LoadString("Strings\\UI:ItemHover_Buff" + i, buffName);
}
try
{
int count = int.Parse(buffName.Substring(0, buffName.IndexOf(' ')));
if (count != 0)
toSpeak = $"{toSpeak} {buffName}\n";
}
catch (Exception) { }
}
}
#endregion
#region Narrate toSpeak
// To prevent it from getting conflicted by two hover texts at the same time, two seperate methods are used.
// For example, sometimes `Welcome to Pierre's` and the items in seeds shop get conflicted causing it to speak infinitely.
if (toSpeak.ToString() != " ")
{
if (StardewModdingAPI.Context.IsPlayerFree)
MainClass.ScreenReader.SayWithChecker(toSpeak.ToString(), true); // Normal Checker
else
MainClass.ScreenReader.SayWithMenuChecker(toSpeak.ToString(), true); // Menu Checker
}
#endregion
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate dialog:\n{e.StackTrace}\n{e.Message}");
}
}
internal static void ExitThisMenuPatch(IClickableMenu __instance)
{
try
{
MainClass.DebugLog($"Closed {__instance.GetType().ToString()} menu, performing cleanup...");
Cleanup(__instance);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup(IClickableMenu menu)
{
switch (menu)
{
case TitleMenu:
TitleMenuPatch.Cleanup();
break;
case CoopMenu:
CoopMenuPatch.Cleanup();
break;
case LoadGameMenu:
LoadGameMenuPatch.Cleanup();
break;
case AdvancedGameOptions:
AdvancedGameOptionsPatch.Cleanup();
break;
case LetterViewerMenu:
LetterViwerMenuPatch.Cleanup();
break;
case LevelUpMenu:
LevelUpMenuPatch.Cleanup();
break;
case Billboard:
BillboardPatch.Cleanup();
break;
case GameMenu:
GameMenuPatch.Cleanup();
ExitPagePatch.Cleanup();
OptionsPagePatch.Cleanup();
SocialPagePatch.Cleanup();
InventoryPagePatch.Cleanup();
CraftingPagePatch.Cleanup();
break;
case JunimoNoteMenu:
JunimoNoteMenuPatch.Cleanup();
break;
case ShopMenu:
ShopMenuPatch.Cleanup();
break;
case ItemGrabMenu:
ItemGrabMenuPatch.Cleanup();
break;
case GeodeMenu:
GeodeMenuPatch.Cleanup();
break;
case CarpenterMenu:
CarpenterMenuPatch.Cleanup();
break;
case PurchaseAnimalsMenu:
PurchaseAnimalsMenuPatch.Cleanup();
break;
case AnimalQueryMenu:
AnimalQueryMenuPatch.Cleanup();
break;
case DialogueBox:
DialogueBoxPatch.Cleanup();
break;
case JojaCDMenu:
JojaCDMenuPatch.Cleanup();
break;
case QuestLog:
QuestLogPatch.Cleaup();
break;
case TailoringMenu:
TailoringMenuPatch.Cleanup();
break;
case ForgeMenu:
ForgeMenuPatch.Cleanup();
break;
case ItemListMenu:
ItemListMenuPatch.Cleanup();
break;
case FieldOfficeMenu:
FieldOfficeMenuPatch.Cleanup();
break;
case MuseumMenu:
MuseumMenuPatch.Cleanup();
break;
case PondQueryMenu:
PondQueryMenuPatch.Cleanup();
break;
case SpecialOrdersBoard:
SpecialOrdersBoardPatch.Cleanup();
break;
}
InventoryUtils.Cleanup();
TextBoxPatch.activeTextBoxes = "";
}
}
}

View File

@@ -0,0 +1,11 @@
namespace stardew_access.Patches
{
internal class InstanceGamePatch
{
internal static void ExitPatch()
{
if (MainClass.ScreenReader != null)
MainClass.ScreenReader.CloseScreenReader();
}
}
}

View File

@@ -0,0 +1,22 @@
using StardewValley;
namespace stardew_access.Patches
{
internal class NPCPatch
{
internal static void DrawAboveAlwaysFrontLayerPatch(NPC __instance, string ___textAboveHead, int ___textAboveHeadTimer)
{
try
{
if (___textAboveHeadTimer > 2900 && ___textAboveHead != null)
{
MainClass.ScreenReader.SayWithChecker($"{__instance.displayName} says {___textAboveHead}", true);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Error in patch:NPCShowTextAboveHeadPatch \n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,43 @@
namespace stardew_access.Patches
{
internal class TextBoxPatch
{
internal static string textBoxQuery = "";
internal static string activeTextBoxes = "";
internal static bool isAnyTextBoxActive => activeTextBoxes != "";
internal static void DrawPatch(StardewValley.Menus.TextBox __instance)
{
try
{
string uniqueIdentifier = $"{__instance.X}:{__instance.Y}:{__instance.Height}:{__instance.Width}";
if (!__instance.Selected)
{
if (activeTextBoxes.Contains(uniqueIdentifier)) activeTextBoxes = activeTextBoxes.Replace(uniqueIdentifier, "");
return;
}
if (!activeTextBoxes.Contains(uniqueIdentifier)) activeTextBoxes += uniqueIdentifier;
bool isEscPressed = StardewValley.Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape);
string toSpeak = __instance.Text;
if (isEscPressed)
{
if (activeTextBoxes.Contains(uniqueIdentifier)) activeTextBoxes = activeTextBoxes.Replace(uniqueIdentifier, "");
__instance.Selected = false;
}
if (textBoxQuery != toSpeak)
{
textBoxQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in DrawPatch() in TextBoxPatch:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,23 @@
namespace stardew_access.Patches
{
internal class TextEntryMenuPatch
{
internal static void DrawPatch(StardewValley.Menus.TextEntryMenu __instance, StardewValley.Menus.TextBox ____target)
{
try
{
TextBoxPatch.DrawPatch(____target);
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in DrawPatch() in TextEntryPatch:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void ClosePatch()
{
TextBoxPatch.activeTextBoxes = "";
}
}
}

View File

@@ -0,0 +1,24 @@
using StardewValley;
using StardewValley.Characters;
namespace stardew_access.Patches
{
internal class TrashBearPatch
{
internal static void CheckActionPatch(TrashBear __instance, bool __result, int ___itemWantedIndex, int ___showWantBubbleTimer)
{
try
{
if (__result) return; // The true `true` value of __result indicates the bear is interactable i.e. when giving the bear the wanted item
if (__instance.sprite.Value.CurrentAnimation != null) return;
string itemName = Game1.objectInformation[___itemWantedIndex].Split('/')[4];
MainClass.ScreenReader.Say(MainClass.Translate("patch.trash_bear.wanted_item", new {trash_bear_name = __instance.displayName, item_name = itemName}), true);
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured TrashBearPatch::CheckActionPatch():\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,114 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class AnimalQueryMenuPatch
{
internal static bool isNarratingAnimalInfo = false;
internal static string animalQueryMenuQuery = "";
internal static AnimalQueryMenu? animalQueryMenu;
internal static FarmAnimal? animalBeingMoved = null;
internal static bool isOnFarm = false;
private static double loveLevel;
internal static void DrawPatch(AnimalQueryMenu __instance, bool ___confirmingSell, FarmAnimal ___animal, TextBox ___textBox, string ___parentName, bool ___movingAnimal, double ___loveLevel)
{
try
{
if (TextBoxPatch.isAnyTextBoxActive) return;
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
isOnFarm = ___movingAnimal;
animalQueryMenu = __instance;
animalBeingMoved = ___animal;
loveLevel = ___loveLevel;
narrateAnimalDetailsOnKeyPress(___animal, ___parentName);
narrateHoveredButton(__instance, ___animal, ___confirmingSell, x, y);
}
catch (System.Exception e)
{
MainClass.ErrorLog($"An error occured in AnimalQueryMenuPatch()->DrawPatch():\n{e.Message}\n{e.StackTrace}");
}
}
private static void narrateAnimalDetailsOnKeyPress(FarmAnimal ___animal, string ___parentName)
{
bool isPrimaryInfoKeyPressed = MainClass.Config.PrimaryInfoKey.JustPressed();
if (!isPrimaryInfoKeyPressed | isNarratingAnimalInfo)
return;
string name = ___animal.displayName;
string type = ___animal.displayType;
int age = (___animal.GetDaysOwned() + 1) / 28 + 1;
string ageText = (age <= 1) ? Game1.content.LoadString("Strings\\UI:AnimalQuery_Age1") : Game1.content.LoadString("Strings\\UI:AnimalQuery_AgeN", age);
string parent = "";
if ((int)___animal.age.Value < (byte)___animal.ageWhenMature.Value)
{
ageText += Game1.content.LoadString("Strings\\UI:AnimalQuery_AgeBaby");
}
if (___parentName != null)
{
parent = Game1.content.LoadString("Strings\\UI:AnimalQuery_Parent", ___parentName);
}
// The loveLevel varies between 0 and 1
// 1 indicates 5 hearts and similarily 0 indicates 0 hearts
// the below code multiplies the loveLevel by 10 and
// the numeric value of the resultent is divided by 2 to give the number of full hearts and
// if its decimal value is above 0.5, then that indicates half a heart
double heartCount = Math.Floor(loveLevel * 10);
double remainder = (loveLevel * 10) % 1;
heartCount /= 2;
if (remainder >= 0.5)
{
heartCount += 0.5;
}
MainClass.DebugLog($"Lovelevel: {loveLevel}");
string heart = MainClass.Translate("patch.animal_query_menu.heart", new { count = heartCount });
isNarratingAnimalInfo = true;
Task.Delay(200).ContinueWith(_ => { isNarratingAnimalInfo = false; }); // Adds delay
MainClass.ScreenReader.Say($"Name: {name} Type: {type} \n\t {ageText} {parent} \n\t {heart}", true);
}
private static void narrateHoveredButton(AnimalQueryMenu __instance, FarmAnimal ___animal, bool ___confirmingSell, int x, int y)
{
string toSpeak = "";
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
toSpeak = "OK button";
else if (__instance.sellButton != null && __instance.sellButton.containsPoint(x, y))
toSpeak = $"Sell for {___animal.getSellPrice()}g button";
else if (___confirmingSell && __instance.yesButton != null && __instance.yesButton.containsPoint(x, y))
toSpeak = "Confirm selling animal";
else if (___confirmingSell && __instance.noButton != null && __instance.noButton.containsPoint(x, y))
toSpeak = "Cancel selling animal";
else if (__instance.moveHomeButton != null && __instance.moveHomeButton.containsPoint(x, y))
toSpeak = "Change home building button";
else if (__instance.allowReproductionButton != null && __instance.allowReproductionButton.containsPoint(x, y))
toSpeak = ((___animal.allowReproduction.Value) ? "Enabled" : "Disabled") + " allow reproduction button";
else if (__instance.textBoxCC != null && __instance.textBoxCC.containsPoint(x, y))
toSpeak = "Animal name text box";
if (animalQueryMenuQuery != toSpeak)
{
animalQueryMenuQuery = toSpeak;
MainClass.ScreenReader.Say($"{toSpeak}", true);
}
}
internal static void Cleanup()
{
AnimalQueryMenuPatch.animalQueryMenuQuery = "";
AnimalQueryMenuPatch.animalQueryMenu = null;
}
}
}

View File

@@ -0,0 +1,174 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class CarpenterMenuPatch
{
internal static CarpenterMenu? carpenterMenu = null;
internal static string carpenterMenuQuery = "";
internal static bool isSayingBlueprintInfo = false;
internal static string prevBlueprintInfo = "";
internal static bool isOnFarm = false, isUpgrading = false, isDemolishing = false, isPainting = false, isConstructing = false, isMoving = false, isMagicalConstruction = false;
internal static void DrawPatch(
CarpenterMenu __instance, bool ___onFarm, List<Item> ___ingredients, int ___price,
List<BluePrint> ___blueprints, int ___currentBlueprintIndex, bool ___upgrading, bool ___demolishing, bool ___moving,
bool ___painting, bool ___magicalConstruction)
{
try
{
isOnFarm = ___onFarm;
carpenterMenu = __instance;
isMagicalConstruction = ___magicalConstruction;
if (!___onFarm)
{
isUpgrading = false;
isDemolishing = false;
isPainting = false;
isMoving = false;
isConstructing = false;
BluePrint currentBlueprint = __instance.CurrentBlueprint;
if (currentBlueprint == null)
return;
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
bool isPrimaryInfoKeyPressed = MainClass.Config.PrimaryInfoKey.JustPressed();
string blueprintInfo = getCurrentBlueprintInfo(currentBlueprint, ___price, ___ingredients);
if (isPrimaryInfoKeyPressed && !isSayingBlueprintInfo)
{
SpeakAndWait(blueprintInfo);
}
else if (prevBlueprintInfo != blueprintInfo)
{
prevBlueprintInfo = blueprintInfo;
SpeakAndWait(blueprintInfo);
}
else
{
narrateHoveredButton(__instance, ___blueprints, ___currentBlueprintIndex, x, y);
}
}
else
{
if (___demolishing)
isDemolishing = true;
else if (___upgrading)
isUpgrading = true;
else if (___painting)
isPainting = true;
else if (___moving)
isMoving = true;
else
isConstructing = true;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static string getCurrentBlueprintInfo(BluePrint currentBlueprint, int ___price, List<Item> ___ingredients)
{
string ingredients = "";
string name = currentBlueprint.displayName;
string upgradeName = currentBlueprint.nameOfBuildingToUpgrade;
string description = currentBlueprint.description;
string price = $"{___price}g";
int width = currentBlueprint.tilesWidth;
int height = currentBlueprint.tilesHeight;
#region Get ingredients
for (int i = 0; i < ___ingredients.Count; i++)
{
string itemName = ___ingredients[i].DisplayName;
int itemStack = ___ingredients[i].Stack;
string itemQuality = "";
int qualityValue = ((StardewValley.Object)___ingredients[i]).Quality;
if (qualityValue == 1)
{
itemQuality = "Silver quality";
}
else if (qualityValue == 2 || qualityValue == 3)
{
itemQuality = "Gold quality";
}
else if (qualityValue >= 4)
{
itemQuality = "Iridium quality";
}
ingredients = $"{ingredients}, {itemStack} {itemName} {itemQuality}";
}
#endregion
return $"{name}, Price: {price}, Ingredients: {ingredients}, Dimensions: {width} width and {height} height, Description: {description}";
}
private static async void SpeakAndWait(string toSpeak)
{
isSayingBlueprintInfo = true;
MainClass.ScreenReader.Say(toSpeak, true);
await Task.Delay(300);
isSayingBlueprintInfo = false;
}
private static void narrateHoveredButton(CarpenterMenu __instance, List<BluePrint> ___blueprints, int ___currentBlueprintIndex, int x, int y)
{
string toSpeak = "";
if (__instance.backButton != null && __instance.backButton.containsPoint(x, y))
{
toSpeak = "Previous Blueprint";
}
else if (__instance.forwardButton != null && __instance.forwardButton.containsPoint(x, y))
{
toSpeak = "Next Blueprint";
}
else if (__instance.demolishButton != null && __instance.demolishButton.containsPoint(x, y))
{
toSpeak = $"Demolish Building" + (__instance.CanDemolishThis(___blueprints[___currentBlueprintIndex]) ? "" : ", cannot demolish building");
}
else if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "Construct Building" + (___blueprints[___currentBlueprintIndex].doesFarmerHaveEnoughResourcesToBuild() ? "" : ", cannot cunstrut building, not enough resources to build.");
}
else if (__instance.moveButton != null && __instance.moveButton.containsPoint(x, y))
{
toSpeak = "Move Building";
}
else if (__instance.paintButton != null && __instance.paintButton.containsPoint(x, y))
{
toSpeak = "Paint Building";
}
else if (__instance.cancelButton != null && __instance.cancelButton.containsPoint(x, y))
{
toSpeak = "Cancel Button";
}
else
{
return;
}
if (carpenterMenuQuery != toSpeak)
{
carpenterMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
internal static void Cleanup()
{
CarpenterMenuPatch.carpenterMenuQuery = "";
CarpenterMenuPatch.isUpgrading = false;
CarpenterMenuPatch.isDemolishing = false;
CarpenterMenuPatch.isPainting = false;
CarpenterMenuPatch.isMoving = false;
CarpenterMenuPatch.isConstructing = false;
CarpenterMenuPatch.carpenterMenu = null;
}
}
}

View File

@@ -0,0 +1,32 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ChooseFromListMenuPatch
{
internal static void DrawPatch(ChooseFromListMenu __instance, List<string> ___options, int ___index, bool ___isJukebox)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
string toSpeak = "";
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
toSpeak = "Select " + (___isJukebox ? Utility.getSongTitleFromCueName(___options[___index]) : ___options[___index]) + " button";
else if (__instance.cancelButton != null && __instance.cancelButton.containsPoint(x, y))
toSpeak = "Cancel button";
else if (__instance.backButton != null && __instance.backButton.containsPoint(x, y))
toSpeak = "Previous option: " + (___isJukebox ? Utility.getSongTitleFromCueName(___options[Math.Max(0, ___index - 1)]) : ___options[Math.Max(0, ___index - 1)]) + " button";
else if (__instance.forwardButton != null && __instance.forwardButton.containsPoint(x, y))
toSpeak = "Next option: " + (___isJukebox ? Utility.getSongTitleFromCueName(___options[Math.Min(___options.Count, ___index + 1)]) : ___options[Math.Min(___options.Count, ___index + 1)]) + " button";
MainClass.ScreenReader.SayWithMenuChecker(toSpeak, true);
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,32 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ConfirmationDialogMenuPatch
{
internal static void DrawPatch(ConfirmationDialog __instance, string ___message)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true);
string toSpeak = ___message;
if (__instance.okButton.containsPoint(x, y))
{
toSpeak += "\n\tOk Button";
}
else if (__instance.cancelButton.containsPoint(x, y))
{
toSpeak += "\n\tCancel Button";
}
MainClass.ScreenReader.SayWithMenuChecker(toSpeak, true);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,55 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ItemListMenuPatch
{
private static string itemListMenuQuery = "";
internal static void DrawPatch(ItemListMenu __instance, string ___title, int ___currentTab, int ___totalValueOfItems, List<Item> ___itemsToList)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
string toSpeak = "", currentList = "";
for (int i = ___currentTab * __instance.itemsPerCategoryPage; i < ___currentTab * __instance.itemsPerCategoryPage + __instance.itemsPerCategoryPage; i++)
{
if (i == 0) currentList = ___title;
if (___itemsToList.Count <= i) continue;
if (___itemsToList[i] == null)
{
currentList = $"{currentList}, \n" + Game1.content.LoadString("Strings\\UI:ItemList_ItemsLostValue", ___totalValueOfItems);
continue;
}
currentList = $"{currentList}, \n {___itemsToList[i].Stack} {___itemsToList[i].DisplayName}";
}
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
toSpeak = $"Page {___currentTab + 1} of {((int)___itemsToList.Count / __instance.itemsPerCategoryPage) + 1} \n {currentList} \n ok button";
else if (__instance.forwardButton != null && __instance.forwardButton.containsPoint(x, y))
toSpeak = "Next page button";
else if (__instance.backButton != null && __instance.backButton.containsPoint(x, y))
toSpeak = "Previous page button";
if (itemListMenuQuery != toSpeak)
{
itemListMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
itemListMenuQuery = "";
}
}
}

View File

@@ -0,0 +1,41 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class LanguageSelectionMenuPatch
{
internal static void DrawPatch(LanguageSelectionMenu __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (__instance.nextPageButton != null && __instance.nextPageButton.containsPoint(x, y))
{
MainClass.ScreenReader.SayWithMenuChecker($"Next Page Button", true);
return;
}
if (__instance.previousPageButton != null && __instance.previousPageButton.containsPoint(x, y))
{
MainClass.ScreenReader.SayWithMenuChecker($"Previous Page Button", true);
return;
}
for (int i = 0; i < __instance.languages.Count; i++)
{
if (__instance.languages[i].containsPoint(x, y))
{
MainClass.ScreenReader.SayWithMenuChecker($"{__instance.languageList[i]} Button", true);
break;
}
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,95 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class LetterViwerMenuPatch
{
private static string currentLetterText = "";
internal static void DrawPatch(LetterViewerMenu __instance)
{
try
{
if (!__instance.IsActive())
return;
narrateLetterContent(__instance);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void narrateLetterContent(LetterViewerMenu __instance)
{
int x = Game1.getMousePosition().X, y = Game1.getMousePosition().Y;
#region Texts in the letter
string message = __instance.mailMessage[__instance.page];
string toSpeak = $"{message}";
if (__instance.ShouldShowInteractable())
{
if (__instance.moneyIncluded > 0)
{
string moneyText = Game1.content.LoadString("Strings\\UI:LetterViewer_MoneyIncluded", __instance.moneyIncluded);
toSpeak += $"\t\n\t ,Included money: {moneyText}";
}
else if (__instance.learnedRecipe != null && __instance.learnedRecipe.Length > 0)
{
string recipeText = Game1.content.LoadString("Strings\\UI:LetterViewer_LearnedRecipe", __instance.cookingOrCrafting);
toSpeak += $"\t\n\t ,Learned Recipe: {recipeText}";
}
}
if (currentLetterText != toSpeak)
{
currentLetterText = toSpeak;
// snap mouse to accept quest button
if (__instance.acceptQuestButton != null && __instance.questID != -1)
{
toSpeak += "\t\n Left click to accept quest.";
__instance.acceptQuestButton.snapMouseCursorToCenter();
}
if (__instance.mailMessage.Count > 1)
toSpeak = $"Page {__instance.page + 1} of {__instance.mailMessage.Count}:\n\t{toSpeak}";
MainClass.ScreenReader.Say(toSpeak, true);
}
#endregion
#region Narrate items given in the mail
if (__instance.ShouldShowInteractable())
{
foreach (ClickableComponent c in __instance.itemsToGrab)
{
if (c.item == null)
continue;
string name = c.item.DisplayName;
if (c.containsPoint(x, y))
MainClass.ScreenReader.SayWithChecker($"Left click to collect {name}", false);
}
}
#endregion
#region Narrate buttons
if (__instance.backButton != null && __instance.backButton.visible && __instance.backButton.containsPoint(x, y))
MainClass.ScreenReader.SayWithChecker($"Previous page button", false);
if (__instance.forwardButton != null && __instance.forwardButton.visible && __instance.forwardButton.containsPoint(x, y))
MainClass.ScreenReader.SayWithChecker($"Next page button", false);
#endregion
}
internal static void Cleanup()
{
currentLetterText = "";
}
}
}

View File

@@ -0,0 +1,109 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class LevelUpMenuPatch
{
private static string currentLevelUpTitle = "";
internal static void DrawPatch(LevelUpMenu __instance, List<int> ___professionsToChoose, List<string> ___leftProfessionDescription, List<string> ___rightProfessionDescription, List<string> ___extraInfoForLevel, List<CraftingRecipe> ___newCraftingRecipes, string ___title, bool ___isActive, bool ___isProfessionChooser)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true);
string leftProfession = "", rightProfession = "", extraInfo = "", newCraftingRecipe = "", toSpeak = "";
if (!__instance.informationUp)
return;
if (__instance.isProfessionChooser)
{
if (___professionsToChoose.Count() == 0) return;
for (int j = 0; j < ___leftProfessionDescription.Count; j++)
{
leftProfession += ___leftProfessionDescription[j] + ", ";
}
for (int i = 0; i < ___rightProfessionDescription.Count; i++)
{
rightProfession += ___rightProfessionDescription[i] + ", ";
}
if (__instance.leftProfession.containsPoint(x, y))
{
if ((MainClass.Config.LeftClickMainKey.JustPressed() || MainClass.Config.LeftClickAlternateKey.JustPressed()) && __instance.readyToClose())
{
Game1.player.professions.Add(___professionsToChoose[0]);
__instance.getImmediateProfessionPerk(___professionsToChoose[0]);
___isActive = false;
__instance.informationUp = false;
___isProfessionChooser = false;
__instance.RemoveLevelFromLevelList();
__instance.exitThisMenu();
return;
}
toSpeak = $"Selected: {leftProfession} Left click to choose.";
}
if (__instance.rightProfession.containsPoint(x, y))
{
if ((MainClass.Config.LeftClickMainKey.JustPressed() || MainClass.Config.LeftClickAlternateKey.JustPressed()) && __instance.readyToClose())
{
Game1.player.professions.Add(___professionsToChoose[1]);
__instance.getImmediateProfessionPerk(___professionsToChoose[1]);
___isActive = false;
__instance.informationUp = false;
___isProfessionChooser = false;
__instance.RemoveLevelFromLevelList();
__instance.exitThisMenu();
return;
}
toSpeak = $"Selected: {rightProfession} Left click to choose.";
}
}
else
{
foreach (string s2 in ___extraInfoForLevel)
{
extraInfo += s2 + ", ";
}
foreach (CraftingRecipe s in ___newCraftingRecipes)
{
string cookingOrCrafting = Game1.content.LoadString("Strings\\UI:LearnedRecipe_" + (s.isCookingRecipe ? "cooking" : "crafting"));
string message = Game1.content.LoadString("Strings\\UI:LevelUp_NewRecipe", cookingOrCrafting, s.DisplayName);
newCraftingRecipe += $"{message}, ";
}
}
if (__instance.okButton.containsPoint(x, y))
{
if (MainClass.Config.LeftClickMainKey.JustPressed() || MainClass.Config.LeftClickAlternateKey.JustPressed())
__instance.okButtonClicked();
toSpeak = $"{___title} {extraInfo} {newCraftingRecipe}. Left click to close.";
}
if (toSpeak != "")
MainClass.ScreenReader.SayWithMenuChecker(toSpeak, true);
else if (__instance.isProfessionChooser && currentLevelUpTitle != $"{___title}. Select a new profession.")
{
MainClass.ScreenReader.SayWithMenuChecker($"{___title}. Select a new profession.", true);
currentLevelUpTitle = $"{___title}. Select a new profession.";
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
currentLevelUpTitle = "";
}
}
}

View File

@@ -0,0 +1,28 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class MineElevatorMenuPatch
{
internal static void DrawPatch(List<ClickableComponent> ___elevators)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
for (int i = 0; i < ___elevators.Count; i++)
{
if (___elevators[i].containsPoint(x, y))
{
MainClass.ScreenReader.SayWithMenuChecker($"{___elevators[i].name} level", true);
break;
}
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,42 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class NamingMenuPatch
{
internal static bool firstTimeInNamingMenu = true;
internal static void DrawPatch(NamingMenu __instance, TextBox ___textBox, string ___title)
{
try
{
if (firstTimeInNamingMenu)
{
firstTimeInNamingMenu = false;
___textBox.Selected = false;
}
if (TextBoxPatch.isAnyTextBoxActive) return;
string toSpeak = "";
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
bool isEscPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape); // For escaping/unselecting from the animal name text box
if (__instance.textBoxCC != null && __instance.textBoxCC.containsPoint(x, y))
toSpeak = $"{___title} text box";
else if (__instance.doneNamingButton != null && __instance.doneNamingButton.containsPoint(x, y))
toSpeak = $"Done naming button";
else if (__instance.randomButton != null && __instance.randomButton.containsPoint(x, y))
toSpeak = $"Random button";
if (toSpeak != "")
MainClass.ScreenReader.SayWithChecker(toSpeak, true);
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,72 @@
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class PondQueryMenuPatch
{
private static string pondQueryMenuQuery = "";
private static bool isNarratingPondInfo = false;
internal static void DrawPatch(PondQueryMenu __instance, StardewValley.Object ____fishItem, FishPond ____pond, string ____statusText, bool ___confirmingEmpty)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
bool isPrimaryInfoKeyPressed = MainClass.Config.PrimaryInfoKey.JustPressed();
string toSpeak = "", extra = "";
if (___confirmingEmpty)
{
if (__instance.yesButton != null && __instance.yesButton.containsPoint(x, y))
toSpeak = "Confirm button";
else if (__instance.noButton != null && __instance.noButton.containsPoint(x, y))
toSpeak = "Cancel button";
}
else
{
if (isPrimaryInfoKeyPressed && !isNarratingPondInfo)
{
string pond_name_text = Game1.content.LoadString("Strings\\UI:PondQuery_Name", ____fishItem.DisplayName);
string population_text = Game1.content.LoadString("Strings\\UI:PondQuery_Population", string.Concat(____pond.FishCount), ____pond.maxOccupants.Value);
bool has_unresolved_needs = ____pond.neededItem.Value != null && ____pond.HasUnresolvedNeeds() && !____pond.hasCompletedRequest.Value;
string bring_text = "";
if (has_unresolved_needs && ____pond.neededItem.Value != null)
bring_text = Game1.content.LoadString("Strings\\UI:PondQuery_StatusRequest_Bring") + $": {____pond.neededItemCount} {____pond.neededItem.Value.DisplayName}";
extra = $"{pond_name_text} {population_text} {bring_text} Status: {____statusText}";
pondQueryMenuQuery = "";
isNarratingPondInfo = true;
Task.Delay(200).ContinueWith(_ => { isNarratingPondInfo = false; });
}
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
toSpeak = "Ok button";
else if (__instance.changeNettingButton != null && __instance.changeNettingButton.containsPoint(x, y))
toSpeak = "Change netting button";
else if (__instance.emptyButton != null && __instance.emptyButton.containsPoint(x, y))
toSpeak = "Empty pond button";
}
if (pondQueryMenuQuery != toSpeak)
{
pondQueryMenuQuery = toSpeak;
MainClass.ScreenReader.Say(extra + " \n\t" + toSpeak, true);
}
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
pondQueryMenuQuery = "";
isNarratingPondInfo = false;
}
}
}

View File

@@ -0,0 +1,113 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class PurchaseAnimalsMenuPatch
{
internal static FarmAnimal? animalBeingPurchased = null;
internal static bool isOnFarm = false;
internal static string purchaseAnimalMenuQuery = "";
internal static PurchaseAnimalsMenu? purchaseAnimalsMenu;
internal static bool firstTimeInNamingMenu = true;
internal static void DrawPatch(PurchaseAnimalsMenu __instance, bool ___onFarm, bool ___namingAnimal, TextBox ___textBox, FarmAnimal ___animalBeingPurchased)
{
try
{
if (TextBoxPatch.isAnyTextBoxActive) return;
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
purchaseAnimalsMenu = __instance;
isOnFarm = ___onFarm;
animalBeingPurchased = ___animalBeingPurchased;
if (___onFarm && ___namingAnimal)
{
narrateNamingMenu(__instance, ___textBox, x, y);
}
else if (___onFarm && !___namingAnimal)
{
firstTimeInNamingMenu = true;
}
else if (!___onFarm && !___namingAnimal)
{
firstTimeInNamingMenu = true;
narratePurchasingMenu(__instance);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static void narrateNamingMenu(PurchaseAnimalsMenu __instance, TextBox ___textBox, int x, int y)
{
string toSpeak = "";
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
toSpeak = "Cancel Button";
}
else if (__instance.doneNamingButton != null && __instance.doneNamingButton.containsPoint(x, y))
{
toSpeak = "OK Button";
}
else if (__instance.randomButton != null && __instance.randomButton.containsPoint(x, y))
{
toSpeak = "Random Name Button";
}
else if (__instance.textBoxCC != null && __instance.textBoxCC.containsPoint(x, y))
{
toSpeak = "Name Text Box";
string? name = ___textBox.Text;
if (name != null)
toSpeak = $"{toSpeak}, Value: {name}";
}
if (purchaseAnimalMenuQuery == toSpeak) return;
purchaseAnimalMenuQuery = toSpeak;
if (firstTimeInNamingMenu)
{
toSpeak = $"Enter the name of animal in the name text box. {toSpeak}";
firstTimeInNamingMenu = false;
}
MainClass.ScreenReader.Say(toSpeak, true);
}
private static void narratePurchasingMenu(PurchaseAnimalsMenu __instance)
{
if (__instance.hovered == null)
return;
string toSpeak = "";
if (((StardewValley.Object)__instance.hovered.item).Type != null)
{
toSpeak = ((StardewValley.Object)__instance.hovered.item).Type;
}
else
{
string displayName = PurchaseAnimalsMenu.getAnimalTitle(__instance.hovered.hoverText);
int price = __instance.hovered.item.salePrice();
string description = PurchaseAnimalsMenu.getAnimalDescription(__instance.hovered.hoverText);
toSpeak = $"{displayName}, Price: {price}g, Description: {description}";
}
if (purchaseAnimalMenuQuery == toSpeak) return;
purchaseAnimalMenuQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
internal static void Cleanup()
{
purchaseAnimalMenuQuery = "";
firstTimeInNamingMenu = true;
purchaseAnimalsMenu = null;
}
}
}

View File

@@ -0,0 +1,46 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class ShippingMenuPatch
{
internal static int prevSlotIndex = -999;
internal static void DrawPatch(ShippingMenu __instance, List<int> ___categoryTotals)
{
try
{
if (__instance.currentPage == -1)
{
int total = ___categoryTotals[5];
string toSpeak;
if (__instance.okButton.containsPoint(Game1.getMouseX(true), Game1.getMouseY(true)))
{
// Perform Left Click
if (MainClass.Config.LeftClickMainKey.JustPressed() || MainClass.Config.LeftClickAlternateKey.JustPressed())
{
Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true));
}
toSpeak = $"{total}g in total. Press left mouse button to save.";
MainClass.ScreenReader.SayWithChecker(toSpeak, true);
}
for (int i = 0; i < __instance.categories.Count; i++)
{
if (__instance.categories[i].containsPoint(Game1.getMouseX(true), Game1.getMouseY(true)))
{
toSpeak = $"Money recieved from {__instance.getCategoryName(i)}: {___categoryTotals[i]}g.";
MainClass.ScreenReader.SayWithChecker(toSpeak, true);
}
}
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -0,0 +1,27 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class TitleTextInputMenuPatch
{
internal static void DrawPatch(TitleTextInputMenu __instance)
{
try
{
string toSpeak = "";
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (__instance.pasteButton != null && __instance.pasteButton.containsPoint(x, y))
toSpeak = $"Paste button";
if (toSpeak != "")
MainClass.ScreenReader.SayWithChecker(toSpeak, true);
}
catch (System.Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
}
}

View File

@@ -1,252 +0,0 @@
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Menus;
using StardewValley.Quests;
namespace stardew_access.Patches
{
internal class QuestPatches
{
private static string currentDailyQuestText = " ";
#region For Special Orders Board
internal static void SpecialOrdersBoardPatch(SpecialOrdersBoard __instance)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY(); // Mouse x and y position
if (__instance.acceptLeftQuestButton.visible && __instance.acceptLeftQuestButton.containsPoint(x, y))
{
string toSpeak = getSpecialOrderDetails(__instance.leftOrder);
toSpeak = $"Left Quest:\n\t{toSpeak}\n\tPress left click to accept this quest.";
MainClass.GetScreenReader().SayWithMenuChecker(toSpeak, true);
return;
}
if (__instance.acceptRightQuestButton.visible && __instance.acceptRightQuestButton.containsPoint(x, y))
{
string toSpeak = getSpecialOrderDetails(__instance.rightOrder);
toSpeak = $"Right Quest:\n\t{toSpeak}\n\tPress left click to accept this quest.";
MainClass.GetScreenReader().SayWithMenuChecker(toSpeak, true);
return;
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
private static string getSpecialOrderDetails(SpecialOrder order)
{
int daysLeft = order.GetDaysLeft();
string description = order.GetDescription();
string objectiveDescription = "";
string name = order.GetName();
int moneyReward = order.GetMoneyReward();
// Get each objectives
for (int i = 0; i < order.GetObjectiveDescriptions().Count; i++)
{
objectiveDescription += order.GetObjectiveDescriptions()[i] + ", \n";
}
string toReturn = $"{name}\n\tDescription:{description}\n\tObjectives: {objectiveDescription}";
if (order.IsTimedQuest())
{
toReturn = $"{toReturn}\n\tTime: {daysLeft} days";
}
if (order.HasMoneyReward())
{
toReturn = $"{toReturn}\n\tReward: {moneyReward}g";
}
return toReturn;
}
#endregion
#region For Normal Billboard in the town
internal static void BillboardPatch(Billboard __instance, bool ___dailyQuestBoard)
{
try
{
if (!___dailyQuestBoard)
{
#region Callender
for (int i = 0; i < __instance.calendarDays.Count; i++)
{
if (__instance.calendarDays[i].containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
string toSpeak = $"Day {i + 1}";
if (__instance.calendarDays[i].name.Length > 0)
{
toSpeak += $", {__instance.calendarDays[i].name}";
}
if (__instance.calendarDays[i].hoverText.Length > 0)
{
toSpeak += $", {__instance.calendarDays[i].hoverText}";
}
if (Game1.dayOfMonth == i + 1)
toSpeak += $", Current";
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
}
#endregion
}
else
{
#region Daily Quest Board
if (Game1.questOfTheDay == null || Game1.questOfTheDay.currentObjective == null || Game1.questOfTheDay.currentObjective.Length == 0)
{
// No quests
string toSpeak = "No quests for today!";
if (currentDailyQuestText != toSpeak)
{
currentDailyQuestText = toSpeak;
MainClass.GetScreenReader().Say(toSpeak, true);
}
}
else
{
SpriteFont font = ((LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.ko) ? Game1.smallFont : Game1.dialogueFont);
string description = Game1.parseText(Game1.questOfTheDay.questDescription, font, 640);
string toSpeak = description;
if (currentDailyQuestText != toSpeak)
{
currentDailyQuestText = toSpeak;
// Snap to accept quest button
if (__instance.acceptQuestButton.visible)
{
toSpeak += "\t\n Left click to accept quest.";
__instance.acceptQuestButton.snapMouseCursorToCenter();
}
MainClass.GetScreenReader().Say(toSpeak, true);
}
}
#endregion
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
#endregion
#region Journal Menu
internal static void QuestLogPatch(QuestLog __instance, int ___questPage, List<List<IQuest>> ___pages, int ___currentPage, IQuest ____shownQuest, List<string> ____objectiveText)
{
try
{
bool snapMouseToRewardBox = false;
if (___questPage == -1)
{
#region Quest Lists
for (int i = 0; i < __instance.questLogButtons.Count; i++)
{
if (___pages.Count() > 0 && ___pages[___currentPage].Count() > i)
{
string name = ___pages[___currentPage][i].GetName();
int daysLeft = ___pages[___currentPage][i].GetDaysLeft();
string toSpeak = $"{name} quest";
if (daysLeft > 0 && ___pages[___currentPage][i].ShouldDisplayAsComplete())
toSpeak += $"\t\n {daysLeft} days left";
toSpeak += ___pages[___currentPage][i].ShouldDisplayAsComplete() ? " completed!" : "";
if (__instance.questLogButtons[i].containsPoint(Game1.getOldMouseX(), Game1.getOldMouseY()))
{
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
}
}
#endregion
}
else
{
#region Individual quest
string description = Game1.parseText(____shownQuest.GetDescription(), Game1.dialogueFont, __instance.width - 128);
string title = ____shownQuest.GetName();
string toSpeak = " ";
if (____shownQuest.ShouldDisplayAsComplete())
{
#region Quest completed menu
toSpeak = $"Quest: {title} Completed!";
if (__instance.HasReward())
{
snapMouseToRewardBox = true;
if (__instance.HasMoneyReward())
{
toSpeak += $"you recieved {____shownQuest.GetMoneyReward()}g";
}
toSpeak += "... left click to collect reward";
}
#endregion
}
else
{
#region Quest in-complete menu
toSpeak = $"Title: {title}. \t\n Description: {description}";
for (int j = 0; j < ____objectiveText.Count; j++)
{
if (____shownQuest != null)
{
_ = ____shownQuest is SpecialOrder;
}
string parsed_text = Game1.parseText(____objectiveText[j], width: __instance.width - 192, whichFont: Game1.dialogueFont);
toSpeak += $"\t\nOrder {j + 1}: {parsed_text} \t\n";
}
if (____shownQuest != null)
{
int daysLeft = ____shownQuest.GetDaysLeft();
if (daysLeft > 0)
toSpeak += $"\t\n{daysLeft} days left.";
}
#endregion
}
// Move mouse to reward button
if (snapMouseToRewardBox)
__instance.rewardBox.snapMouseCursorToCenter();
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
#endregion
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
#endregion
internal static void resetGlobalVars()
{
currentDailyQuestText = " ";
}
}
}

View File

@@ -0,0 +1,102 @@
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class BillboardPatch
{
private static string billboardQueryKey = "";
internal static void DrawPatch(Billboard __instance, bool ___dailyQuestBoard)
{
try
{
if (___dailyQuestBoard)
{
narrateDailyQuestBoard(__instance);
}
else
{
narrateCalendar(__instance);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static void narrateCalendar(Billboard __instance)
{
for (int i = 0; i < __instance.calendarDays.Count; i++)
{
if (!__instance.calendarDays[i].containsPoint(Game1.getMouseX(true), Game1.getMouseY(true)))
continue;
string toSpeak = $"Day {i + 1}";
string currentYearNMonth = $"of {Game1.CurrentSeasonDisplayName}, {Game1.content.LoadString("Strings\\UI:Billboard_Year", Game1.year)}";
if (__instance.calendarDays[i].name.Length > 0)
{
toSpeak += $", {__instance.calendarDays[i].name}";
}
if (__instance.calendarDays[i].hoverText.Length > 0)
{
toSpeak += $", {__instance.calendarDays[i].hoverText}";
}
if (Game1.dayOfMonth == i + 1)
toSpeak = $"Current {toSpeak}";
if (billboardQueryKey != toSpeak)
{
billboardQueryKey = toSpeak;
if (i == 0) toSpeak = $"{toSpeak} {currentYearNMonth}";
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
}
private static void narrateDailyQuestBoard(Billboard __instance)
{
if (Game1.questOfTheDay == null || Game1.questOfTheDay.currentObjective == null || Game1.questOfTheDay.currentObjective.Length == 0)
{
// No quests
string toSpeak = "No quests for today!";
if (billboardQueryKey != toSpeak)
{
billboardQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
else
{
SpriteFont font = ((LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.ko) ? Game1.smallFont : Game1.dialogueFont);
string description = Game1.parseText(Game1.questOfTheDay.questDescription, font, 640);
string toSpeak = description;
if (billboardQueryKey != toSpeak)
{
billboardQueryKey = toSpeak;
// Snap to accept quest button
if (__instance.acceptQuestButton.visible)
{
toSpeak += "\t\n Left click to accept quest.";
__instance.acceptQuestButton.snapMouseCursorToCenter();
}
MainClass.ScreenReader.Say(toSpeak, true);
}
}
}
internal static void Cleanup()
{
billboardQueryKey = "";
}
}
}

View File

@@ -0,0 +1,164 @@
using StardewValley;
using StardewValley.Menus;
using StardewValley.Quests;
namespace stardew_access.Patches
{
// a.k.a. Journal Menu
internal class QuestLogPatch
{
internal static string questLogQuery = "";
internal static bool isNarratingQuestInfo = false;
internal static bool firstTimeInIndividualQuest = true;
internal static void DrawPatch(QuestLog __instance, int ___questPage, List<List<IQuest>> ___pages, int ___currentPage, IQuest ____shownQuest, List<string> ____objectiveText)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (___questPage == -1)
{
narrateQuestList(__instance, ___pages, ___currentPage, x, y);
}
else
{
narrateIndividualQuest(__instance, ___currentPage, ____shownQuest, ____objectiveText, x, y);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static void narrateQuestList(QuestLog __instance, List<List<IQuest>> ___pages, int ___currentPage, int x, int y)
{
string toSpeak = "";
if (!firstTimeInIndividualQuest) firstTimeInIndividualQuest = true;
if (__instance.backButton != null && __instance.backButton.visible && __instance.backButton.containsPoint(x, y))
toSpeak = "Previous page button";
else if (__instance.forwardButton != null && __instance.forwardButton.visible && __instance.forwardButton.containsPoint(x, y))
toSpeak = "Next page button";
else if (__instance.upperRightCloseButton != null && __instance.upperRightCloseButton.visible && __instance.upperRightCloseButton.containsPoint(x, y))
toSpeak = "Close menu button";
else
{
for (int i = 0; i < __instance.questLogButtons.Count; i++)
{
if (___pages.Count() <= 0 || ___pages[___currentPage].Count() <= i)
continue;
if (!__instance.questLogButtons[i].containsPoint(x, y))
continue;
string name = ___pages[___currentPage][i].GetName();
int daysLeft = ___pages[___currentPage][i].GetDaysLeft();
toSpeak = $"{name}";
if (daysLeft > 0 && ___pages[___currentPage][i].ShouldDisplayAsComplete())
toSpeak += $"\t\n {daysLeft} days left";
toSpeak += ___pages[___currentPage][i].ShouldDisplayAsComplete() ? " completed!" : "";
break;
}
}
if (questLogQuery != toSpeak)
{
questLogQuery = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
private static void narrateIndividualQuest(QuestLog __instance, int ___currentPage, IQuest ____shownQuest, List<string> ____objectiveText, int x, int y)
{
bool isPrimaryInfoKeyPressed = MainClass.Config.PrimaryInfoKey.JustPressed();
bool containsReward = __instance.HasReward() || __instance.HasMoneyReward();
string description = Game1.parseText(____shownQuest.GetDescription(), Game1.dialogueFont, __instance.width - 128);
string title = ____shownQuest.GetName();
string toSpeak = "";
string extra = "";
if (firstTimeInIndividualQuest || (isPrimaryInfoKeyPressed && !isNarratingQuestInfo))
{
if (firstTimeInIndividualQuest)
toSpeak = "Back button";
if (____shownQuest.ShouldDisplayAsComplete())
{
#region Quest completed menu
extra = $"Quest: {title} Completed!";
if (__instance.HasMoneyReward())
extra += $"you recieved {____shownQuest.GetMoneyReward()}g";
#endregion
}
else
{
#region Quest in-complete menu
extra = $"Title: {title}. \t\n Description: {description}";
for (int j = 0; j < ____objectiveText.Count; j++)
{
string parsed_text = Game1.parseText(____objectiveText[j], width: __instance.width - 192, whichFont: Game1.dialogueFont);
if (____shownQuest != null && ____shownQuest is SpecialOrder)
{
OrderObjective order_objective = ((SpecialOrder)____shownQuest).objectives[j];
if (order_objective.GetMaxCount() > 1 && order_objective.ShouldShowProgress())
parsed_text += "\n\t" + order_objective.GetCount() + " of " + order_objective.GetMaxCount() + " completed";
}
extra += $"\t\nOrder {j + 1}: {parsed_text} \t\n";
}
if (____shownQuest != null)
{
int daysLeft = ____shownQuest.GetDaysLeft();
if (daysLeft > 0)
extra += $"\t\n{daysLeft} days left.";
}
#endregion
}
isNarratingQuestInfo = true;
Task.Delay(200).ContinueWith(_ => { isNarratingQuestInfo = false; });
questLogQuery = "";
}
if (!firstTimeInIndividualQuest)
{
if (__instance.backButton != null && __instance.backButton.visible && __instance.backButton.containsPoint(x, y))
toSpeak = (___currentPage > 0) ? "Previous page button" : "Back button";
else if (__instance.forwardButton != null && __instance.forwardButton.visible && __instance.forwardButton.containsPoint(x, y))
toSpeak = "Next page button";
else if (__instance.cancelQuestButton != null && __instance.cancelQuestButton.visible && __instance.cancelQuestButton.containsPoint(x, y))
toSpeak = "Cancel quest button";
else if (__instance.upperRightCloseButton != null && __instance.upperRightCloseButton.visible && __instance.upperRightCloseButton.containsPoint(x, y))
toSpeak = "Close menu button";
else if (containsReward && __instance.rewardBox.containsPoint(x, y))
toSpeak = "Left click to collect reward";
}
if (firstTimeInIndividualQuest || (questLogQuery != toSpeak))
{
questLogQuery = toSpeak;
MainClass.ScreenReader.Say(extra + " \n\t" + toSpeak, true);
if (firstTimeInIndividualQuest) firstTimeInIndividualQuest = false;
}
}
internal static void Cleaup()
{
questLogQuery = "";
isNarratingQuestInfo = false;
firstTimeInIndividualQuest = true;
}
}
}

View File

@@ -0,0 +1,84 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class SpecialOrdersBoardPatch
{
private static string specialOrdersBoardQueryKey = "";
internal static void DrawPatch(SpecialOrdersBoard __instance)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
if (__instance.acceptLeftQuestButton.visible && __instance.acceptLeftQuestButton.containsPoint(x, y))
{
string toSpeak = getSpecialOrderDetails(__instance.leftOrder);
toSpeak = $"Left Quest:\n\t{toSpeak}\n\tPress left click to accept this quest.";
Speak(toSpeak);
return;
}
if (__instance.acceptRightQuestButton.visible && __instance.acceptRightQuestButton.containsPoint(x, y))
{
string toSpeak = getSpecialOrderDetails(__instance.rightOrder);
toSpeak = $"Right Quest:\n\t{toSpeak}\n\tPress left click to accept this quest.";
Speak(toSpeak);
return;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}");
}
}
private static string getSpecialOrderDetails(SpecialOrder order)
{
int daysLeft = order.GetDaysLeft();
string description = order.GetDescription();
string objectiveDescription = "";
string name = order.GetName();
int moneyReward = order.GetMoneyReward();
// Get each objectives
for (int i = 0; i < order.GetObjectiveDescriptions().Count; i++)
{
objectiveDescription += order.GetObjectiveDescriptions()[i] + ", \n";
}
string toReturn = $"{name}\n\tDescription:{description}\n\tObjectives: {objectiveDescription}";
if (order.IsTimedQuest())
{
toReturn = $"{toReturn}\n\tTime: {daysLeft} days";
}
if (order.HasMoneyReward())
{
toReturn = $"{toReturn}\n\tReward: {moneyReward}g";
}
return toReturn;
}
private static void Speak(string toSpeak)
{
if (specialOrdersBoardQueryKey == toSpeak) return;
specialOrdersBoardQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
internal static void Cleanup()
{
specialOrdersBoardQueryKey = "";
}
}
}

View File

@@ -1,677 +0,0 @@
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Menus;
using static StardewValley.Menus.LoadGameMenu;
namespace stardew_access.Patches
{
internal class TitleMenuPatches
{
private static int saveGameIndex = -1;
private static bool isRunning = false;
private const int MAX_COMPONENTS = 20;
internal static void CoopMenuPatch(CoopMenu __instance, CoopMenu.Tab ___currentTab)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY();
string toSpeak = " ";
#region Join/Host Button (Important! This should be checked before checking other buttons)
if (__instance.slotButtons[0].containsPoint(x, y))
{
if (___currentTab == CoopMenu.Tab.JOIN_TAB)
toSpeak = "Join lan game";
if (___currentTab == CoopMenu.Tab.HOST_TAB)
toSpeak = "Host new farm";
}
#endregion
#region Other Buttons
if (__instance.joinTab.containsPoint(x, y))
{
toSpeak = "Join Tab Button";
}
else if (__instance.hostTab.containsPoint(x, y))
{
toSpeak = "Host Tab Button";
}
else if (__instance.refreshButton.containsPoint(x, y))
{
toSpeak = "Refresh Button";
}
#endregion
if (toSpeak != " ")
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void TitleMenuPatch(TitleMenu __instance, bool ___isTransitioningButtons)
{
try
{
if (___isTransitioningButtons)
return;
string toSpeak = "";
__instance.buttons.ForEach(component =>
{
if (component.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
string name = component.name;
string label = component.label;
toSpeak = $"{name} {label} Button";
}
});
if (__instance.muteMusicButton.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
toSpeak = "Mute Music Button";
}
if (__instance.aboutButton.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
toSpeak = "About Button";
}
if (__instance.languageButton.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
toSpeak = "Language Button";
}
if (__instance.windowedButton.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
toSpeak = "Fullscreen toggle Button";
}
if (TitleMenu.subMenu != null && __instance.backButton.containsPoint(Game1.getMouseX(), Game1.getMouseY()))
{
string text = "Back Button";
MainClass.GetScreenReader().SayWithChecker(text, true);
}
if (TitleMenu.subMenu == null && toSpeak != "")
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void LoadGameMenuPatch(SaveFileSlot __instance, LoadGameMenu ___menu, int i)
{
try
{
int x = Game1.getMouseX(), y = Game1.getMouseY();
if (___menu.slotButtons[i].containsPoint(x, y))
{
if (__instance.Farmer != null)
{
#region Farms
if (___menu.deleteButtons.Count > 0 && ___menu.deleteButtons[i].containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithChecker($"Delete {__instance.Farmer.farmName} Farm", true);
return;
}
if (___menu.deleteConfirmationScreen)
{
// Used diff. functions to narrate to prevent it from speaking the message again on selecting another button.
string message = "Really delete farm?";
MainClass.GetScreenReader().SayWithChecker(message, true);
if (___menu.okDeleteButton.containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker("Ok Button", false);
}
else if (___menu.cancelDeleteButton.containsPoint(x, y))
{
MainClass.GetScreenReader().SayWithMenuChecker("Cancel Button", false);
}
return;
}
String farmerName = __instance.Farmer.displayName;
String farmName = __instance.Farmer.farmName;
String money = __instance.Farmer.Money.ToString();
String hoursPlayed = Utility.getHoursMinutesStringFromMilliseconds(__instance.Farmer.millisecondsPlayed);
string dateStringForSaveGame = ((!__instance.Farmer.dayOfMonthForSaveGame.HasValue ||
!__instance.Farmer.seasonForSaveGame.HasValue ||
!__instance.Farmer.yearForSaveGame.HasValue) ? __instance.Farmer.dateStringForSaveGame : Utility.getDateStringFor(__instance.Farmer.dayOfMonthForSaveGame.Value, __instance.Farmer.seasonForSaveGame.Value, __instance.Farmer.yearForSaveGame.Value));
string toSpeak = $"{farmName} Farm Selected, \t\n Farmer:{farmerName}, \t\nMoney:{money}, \t\nHours Played:{hoursPlayed}, \t\nDate:{dateStringForSaveGame}";
MainClass.GetScreenReader().SayWithChecker(toSpeak, true);
#endregion
}
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
internal static void NewGameMenuPatch(CharacterCustomization __instance, bool ___skipIntro)
{
try
{
bool isNextArrowPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Right);
bool isPrevArrowPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Left);
if (isNextArrowPressed && !isRunning)
{
_ = CycleThroughItems(true, __instance, ___skipIntro);
}
else if (isPrevArrowPressed && !isRunning)
{
_ = CycleThroughItems(false, __instance, ___skipIntro);
}
}
catch (Exception e)
{
MainClass.GetMonitor().Log($"Unable to narrate Text:\n{e.Message}\n{e.StackTrace}", LogLevel.Error);
}
}
private static async Task CycleThroughItems(bool increase, CharacterCustomization __instance, bool ___skipIntro)
{
isRunning = true;
string toSpeak = " ";
if (increase)
{
saveGameIndex++;
if (saveGameIndex > MAX_COMPONENTS)
saveGameIndex = 1;
}
else
{
saveGameIndex--;
if (saveGameIndex < 1)
saveGameIndex = MAX_COMPONENTS;
}
switch (saveGameIndex)
{
case 1:
{
#region Skip if button is not available
if (!__instance.nameBoxCC.visible)
{
if (increase)
{
++saveGameIndex;
goto case 2;
}
else
{
--saveGameIndex;
goto case MAX_COMPONENTS;
}
}
#endregion
__instance.nameBoxCC.snapMouseCursorToCenter();
toSpeak = "Enter Farmer's Name";
}
break;
case 2:
{
#region Skip if button is not available
if (!__instance.farmnameBoxCC.visible)
{
if (increase)
{
++saveGameIndex;
goto case 3;
}
else
{
--saveGameIndex;
goto case 1;
}
}
#endregion
__instance.farmnameBoxCC.snapMouseCursorToCenter();
toSpeak = "Enter Farm's Name";
}
break;
case 3:
{
#region Skip if button is not available
if (!__instance.favThingBoxCC.visible)
{
if (increase)
{
++saveGameIndex;
goto case 4;
}
else
{
--saveGameIndex;
goto case 2;
}
}
#endregion
__instance.favThingBoxCC.snapMouseCursorToCenter();
toSpeak = "Enter Favourite Thing";
}
break;
case 4:
{
#region Skip if button is not available
if (!__instance.skipIntroButton.visible)
{
if (increase)
{
++saveGameIndex;
goto case 5;
}
else
{
--saveGameIndex;
goto case 3;
}
}
#endregion
__instance.skipIntroButton.snapMouseCursor();
toSpeak = (___skipIntro ? "Enabled" : "Disabled") + " Skip Intro Button";
}
break;
case 5:
{
#region Skip if button is not available
if (!__instance.randomButton.visible)
{
if (increase)
{
++saveGameIndex;
goto case 6;
}
else
{
--saveGameIndex;
goto case 5;
}
}
#endregion
__instance.randomButton.snapMouseCursor();
toSpeak = "Random Skin Button";
break;
}
case 6:
{
#region Skip if button is not available
if (__instance.genderButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 8;
}
else
{
--saveGameIndex;
goto case 6;
}
}
#endregion
__instance.genderButtons[0].snapMouseCursor();
toSpeak = "Gender Male Button";
break;
}
case 7:
{
#region Skip if button is not available
if (__instance.genderButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 8;
}
else
{
--saveGameIndex;
goto case 6;
}
}
#endregion
__instance.genderButtons[1].snapMouseCursor();
toSpeak = "Gender Female Button";
break;
}
case 8:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 9;
}
else
{
--saveGameIndex;
goto case 7;
}
}
#endregion
__instance.farmTypeButtons[0].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[0]);
break;
}
case 9:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 10;
}
else
{
--saveGameIndex;
goto case 8;
}
}
#endregion
__instance.farmTypeButtons[1].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[1]);
break;
}
case 10:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 11;
}
else
{
--saveGameIndex;
goto case 9;
}
}
#endregion
__instance.farmTypeButtons[2].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[2]);
break;
}
case 11:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 12;
}
else
{
--saveGameIndex;
goto case 10;
}
}
#endregion
__instance.farmTypeButtons[3].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[3]);
break;
}
case 12:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 13;
}
else
{
--saveGameIndex;
goto case 11;
}
}
#endregion
__instance.farmTypeButtons[4].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[4]);
break;
}
case 13:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 14;
}
else
{
--saveGameIndex;
goto case 12;
}
}
#endregion
__instance.farmTypeButtons[5].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[5]);
break;
}
case 14:
{
#region Skip if button is not available
if (__instance.farmTypeButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 15;
}
else
{
--saveGameIndex;
goto case 13;
}
}
#endregion
__instance.farmTypeButtons[6].snapMouseCursor();
toSpeak = getFarmHoverText(__instance.farmTypeButtons[6]);
break;
}
case 15:
{
#region Skip if button is not available
if (__instance.farmTypeNextPageButton == null)
{
if (increase)
{
++saveGameIndex;
goto case 16;
}
else
{
--saveGameIndex;
goto case 14;
}
}
#endregion
__instance.farmTypeNextPageButton.snapMouseCursor();
toSpeak = "Next Farm Type Page Button";
break;
}
case 16:
{
#region Skip if button is not available
if (__instance.farmTypePreviousPageButton == null)
{
if (increase)
{
++saveGameIndex;
goto case 17;
}
else
{
--saveGameIndex;
goto case 15;
}
}
#endregion
__instance.farmTypePreviousPageButton.snapMouseCursor();
toSpeak = "Previous Farm Type Page Button";
break;
}
case 17:
{
#region Skip if button is not available
if (__instance.cabinLayoutButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 18;
}
else
{
--saveGameIndex;
goto case 16;
}
}
#endregion
__instance.cabinLayoutButtons[0].snapMouseCursor();
toSpeak = "Cabin layout nearby";
break;
}
case 18:
{
#region Skip if button is not available
if (__instance.cabinLayoutButtons.Count <= 0)
{
if (increase)
{
++saveGameIndex;
goto case 19;
}
else
{
--saveGameIndex;
goto case 17;
}
}
#endregion
__instance.cabinLayoutButtons[1].snapMouseCursor();
toSpeak = "Cabin layout separate";
break;
}
case 19:
{
#region Skip if button is not available
if (!__instance.okButton.visible)
{
if (increase)
{
++saveGameIndex;
goto case 18;
}
else
{
--saveGameIndex;
goto case 20;
}
}
#endregion
__instance.okButton.snapMouseCursor();
toSpeak = "Ok Button";
}
break;
case 20:
{
#region Exit if button is not available
if (!__instance.backButton.visible)
{
break;
}
#endregion
__instance.backButton.snapMouseCursor();
toSpeak = "Back Button";
}
break;
}
if (toSpeak != " ")
{
MainClass.GetScreenReader().Say(toSpeak, true);
}
await Task.Delay(200);
isRunning = false;
}
private static string getFarmHoverText(ClickableTextureComponent farm)
{
string hoverTitle = " ", hoverText = " ";
if (!farm.name.Contains("Gray"))
{
if (farm.hoverText.Contains('_'))
{
hoverTitle = farm.hoverText.Split('_')[0];
hoverText = farm.hoverText.Split('_')[1];
}
else
{
hoverTitle = " ";
hoverText = farm.hoverText;
}
}
else
{
if (farm.name.Contains("Gray"))
{
hoverText = "Reach level 10 " + Game1.content.LoadString("Strings\\UI:Character_" + farm.name.Split('_')[1]) + " to unlock.";
}
}
return $"{hoverTitle}: {hoverText}";
}
}
}

View File

@@ -0,0 +1,85 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class AdvancedGameOptionsPatch
{
public static string advancedGameOptionsQueryKey = " ";
internal static void DrawPatch(AdvancedGameOptions __instance)
{
try
{
int currentItemIndex = Math.Max(0, Math.Min(__instance.options.Count - 7, __instance.currentItemIndex));
int x = Game1.getMouseX(true), y = Game1.getMouseY(true);
if (__instance.okButton != null && __instance.okButton.containsPoint(x, y))
{
string toSpeak = "OK Button";
if (advancedGameOptionsQueryKey != toSpeak)
{
advancedGameOptionsQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
for (int i = 0; i < __instance.optionSlots.Count; i++)
{
if (!__instance.optionSlots[i].bounds.Contains(x, y)
|| currentItemIndex + i >= __instance.options.Count
|| !__instance.options[currentItemIndex + i].bounds.Contains(x - __instance.optionSlots[i].bounds.X, y - __instance.optionSlots[i].bounds.Y))
continue;
OptionsElement optionsElement = __instance.options[currentItemIndex + i];
string toSpeak = optionsElement.label;
if (optionsElement is OptionsButton)
toSpeak = $" {toSpeak} Button";
else if (optionsElement is OptionsCheckbox)
toSpeak = (((OptionsCheckbox)optionsElement).isChecked ? "Enabled" : "Disabled") + $" {toSpeak} Checkbox";
else if (optionsElement is OptionsDropDown)
toSpeak = $"{toSpeak} Dropdown, option {((OptionsDropDown)optionsElement).dropDownDisplayOptions[((OptionsDropDown)optionsElement).selectedOption]} selected";
else if (optionsElement is OptionsSlider)
toSpeak = $"{((OptionsSlider)optionsElement).value}% {toSpeak} Slider";
else if (optionsElement is OptionsPlusMinus)
toSpeak = $"{((OptionsPlusMinus)optionsElement).displayOptions[((OptionsPlusMinus)optionsElement).selected]} selected of {toSpeak}";
else if (optionsElement is OptionsInputListener)
{
string buttons = "";
((OptionsInputListener)optionsElement).buttonNames.ForEach(name => { buttons += $", {name}"; });
toSpeak = $"{toSpeak} is bound to {buttons}. Left click to change.";
}
else if (optionsElement is OptionsTextEntry)
{
toSpeak = $"Seed text box";
}
else
{
if (toSpeak.Contains(":"))
toSpeak = toSpeak.Replace(":", "");
toSpeak = $"{toSpeak} Options:";
}
if (advancedGameOptionsQueryKey != toSpeak)
{
advancedGameOptionsQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in advanced game menu patch:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
advancedGameOptionsQueryKey = "";
}
}
}

View File

@@ -0,0 +1,834 @@
using System.Text.Json;
using StardewValley;
using StardewValley.Menus;
using static stardew_access.Features.Utils;
namespace stardew_access.Patches
{
internal class CharacterCustomizationMenuPatch
{
private static bool isRunning = false;
private static int saveGameIndex = -1;
private static string characterCreationMenuQueryKey = " ";
private static string prevPants = " ";
private static string prevShirt = " ";
private static string prevHair = " ";
private static string prevAccessory = " ";
private static string prevSkin = " ";
private static string prevEyeColor = " ";
private static string prevEyeColorHue = " ";
private static string prevEyeColorSaturation = " ";
private static string prevEyeColorValue = " ";
private static string prevHairColor = " ";
private static string prevHairColorHue = " ";
private static string prevHairColorSaturation = " ";
private static string prevHairColorValue = " ";
private static string prevPantsColor = " ";
private static string prevPantsColorHue = " ";
private static string prevPantsColorSaturation = " ";
private static string prevPantsColorValue = " ";
private static string prevPet = " ";
private static bool characterDesignToggle = false;
private static bool characterDesignToggleShouldSpeak = true;
private static ClickableComponent? currentComponent = null;
private static Dictionary<string, Dictionary<int, string>> descriptions
{
get
{
if (_descriptions == null)
{
_descriptions = LoadDescriptionJson();
}
return _descriptions;
}
}
private static Dictionary<string, Dictionary<int, string>>? _descriptions;
private static Dictionary<string, Dictionary<int, string>> LoadDescriptionJson()
{
MainClass.DebugLog("Attempting to load json");
JsonElement jsonElement = LoadJsonFile("new-character-appearance-descriptions.json");
if (jsonElement.ValueKind == JsonValueKind.Undefined)
{
return new Dictionary<string, Dictionary<int, string>>();
}
Dictionary<string, Dictionary<int, string>> result = new Dictionary<string, Dictionary<int, string>>();
foreach (JsonProperty category in jsonElement.EnumerateObject())
{
Dictionary<int, string> innerDictionary = new Dictionary<int, string>();
foreach (JsonProperty item in category.Value.EnumerateObject())
{
int index = int.Parse(item.Name);
innerDictionary[index] = item.Value.GetString() ?? "";
}
result[category.Name] = innerDictionary;
MainClass.InfoLog($"Loaded key '{category.Name}' with {innerDictionary.Count} entries in the sub dictionary.");
}
return result;
}
internal static void DrawPatch(CharacterCustomization __instance, bool ___skipIntro,
ClickableComponent ___startingCabinsLabel, ClickableComponent ___difficultyModifierLabel, TextBox ___nameBox,
TextBox ___farmnameBox, TextBox ___favThingBox)
{
try
{
if (TextBoxPatch.isAnyTextBoxActive) return;
bool isEscPressed = Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.Escape); // For escaping/unselecting from the animal name text box
string toSpeak = "";
if (characterDesignToggleShouldSpeak)
{
toSpeak = "Press left control + space to toggle character appearance controls";
characterDesignToggleShouldSpeak = false;
}
string itemsToSpeak = "";
string changesToSpeak = "";
if (MainClass.Config.CharacterCreationMenuNextKey.JustPressed() && !isRunning)
{
isRunning = true;
itemsToSpeak = CycleThroughItems(true, __instance, ___skipIntro, ___startingCabinsLabel, ___difficultyModifierLabel, ___nameBox, ___farmnameBox, ___favThingBox);
if (itemsToSpeak != "")
toSpeak = $"{itemsToSpeak} \n {toSpeak}";
Task.Delay(200).ContinueWith(_ => { isRunning = false; });
}
else if (MainClass.Config.CharacterCreationMenuPreviousKey.JustPressed() && !isRunning)
{
isRunning = true;
toSpeak = CycleThroughItems(false, __instance, ___skipIntro, ___startingCabinsLabel, ___difficultyModifierLabel, ___nameBox, ___farmnameBox, ___favThingBox);
Task.Delay(200).ContinueWith(_ => { isRunning = false; });
}
else if (characterDesignToggle && MainClass.Config.CharacterCreationMenuSliderIncreaseKey.JustPressed() && !isRunning)
{
isRunning = true;
AdjustCurrentSlider(true, __instance);
Task.Delay(200).ContinueWith(_ => { isRunning = false; });
}
else if (characterDesignToggle && MainClass.Config.CharacterCreationMenuSliderLargeIncreaseKey.JustPressed() && !isRunning)
{
isRunning = true;
AdjustCurrentSlider(true, __instance, 10);
Task.Delay(200).ContinueWith(_ => { isRunning = false; });
}
else if (characterDesignToggle && MainClass.Config.CharacterCreationMenuSliderDecreaseKey.JustPressed() && !isRunning)
{
isRunning = true;
AdjustCurrentSlider(false, __instance);
Task.Delay(200).ContinueWith(_ => { isRunning = false; });
}
else if (characterDesignToggle && MainClass.Config.CharacterCreationMenuSliderLargeDecreaseKey.JustPressed() && !isRunning)
{
isRunning = true;
AdjustCurrentSlider(false, __instance, 10);
Task.Delay(200).ContinueWith(_ => { isRunning = false; });
}
else if (Game1.input.GetKeyboardState().IsKeyDown(Microsoft.Xna.Framework.Input.Keys.LeftControl) && MainClass.Config.CharacterCreationMenuDesignToggleKey.JustPressed() && !isRunning)
{
string displayState = "";
characterDesignToggle = !characterDesignToggle;
saveGameIndex = Math.Min(saveGameIndex, 5); // move to random skin button if focus was beyond that point
if (characterDesignToggle)
{
displayState = "shown";
}
else
{
displayState = "hidden";
}
toSpeak = $"Character design controls {displayState}. \n {toSpeak}";
}
changesToSpeak = getChangesToSpeak(__instance);
if (changesToSpeak != "")
toSpeak = $"{toSpeak} \n {changesToSpeak}";
if (characterCreationMenuQueryKey != toSpeak && toSpeak.Trim() != "")
{
characterCreationMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in character customization menu patch:\n{e.Message}\n{e.StackTrace}");
}
}
private static string getChangesToSpeak(CharacterCustomization __instance)
{
string toSpeak = "";
string currentPet = GetCurrentPet();
string currentSkin = GetCurrentSkin();
string currentHair = GetCurrentHair();
string currentShirt = GetCurrentShirt();
string currentPants = GetCurrentPants();
string currentAccessory = GetCurrentAccessory();
string currentEyeColor = GetCurrentEyeColor();
string currentEyeColorHue = GetCurrentEyeColorHue(__instance);
string currentEyeColorSaturation = GetCurrentEyeColorSaturation(__instance);
string currentEyeColorValue = GetCurrentEyeColorValue(__instance);
string currentHairColor = GetCurrentHairColor();
string currentHairColorHue = GetCurrentHairColorHue(__instance);
string currentHairColorSaturation = GetCurrentHairColorSaturation(__instance);
string currentHairColorValue = GetCurrentHairColorValue(__instance);
string currentPantsColor = GetCurrentPantsColor();
string currentPantsColorHue = GetCurrentPantsColorHue(__instance);
string currentPantsColorSaturation = GetCurrentPantsColorSaturation(__instance);
string currentPantsColorValue = GetCurrentPantsColorValue(__instance);
if (characterDesignToggle)
{
if (prevSkin != currentSkin)
{
prevSkin = currentSkin;
if (currentSkin != "")
toSpeak = $"{toSpeak} \n {currentSkin}";
}
if (prevHair != currentHair)
{
prevHair = currentHair;
if (currentHair != "")
toSpeak = $"{toSpeak} \n {currentHair}";
}
if (prevShirt != currentShirt)
{
prevShirt = currentShirt;
if (currentShirt != "")
toSpeak = $"{toSpeak} \n {currentShirt}";
}
if (prevPants != currentPants)
{
prevPants = currentPants;
if (currentPants != "")
toSpeak = $"{toSpeak} \n {currentPants}";
}
if (prevAccessory != currentAccessory)
{
prevAccessory = currentAccessory;
if (currentAccessory != "")
toSpeak = $"{toSpeak} \n {currentAccessory}";
}
if (prevEyeColorHue != currentEyeColorHue)
{
if (currentComponent != null && currentComponent.myID == 522)
{
prevEyeColorHue = currentEyeColorHue;
if (currentEyeColorHue != "")
toSpeak = $"{toSpeak} \n Hue: {currentEyeColorHue}";
}
else
{
prevEyeColorHue = "";
}
}
if (prevEyeColorSaturation != currentEyeColorSaturation)
{
if (currentComponent != null && currentComponent.myID == 523)
{
prevEyeColorSaturation = currentEyeColorSaturation;
if (currentEyeColorSaturation != "")
toSpeak = $"{toSpeak} \n Saturation: {currentEyeColorSaturation}";
}
else
{
prevEyeColorSaturation = "";
}
}
if (prevEyeColorValue != currentEyeColorValue)
{
if (currentComponent != null && currentComponent.myID == 524)
{
prevEyeColorValue = currentEyeColorValue;
if (currentEyeColorValue != "")
toSpeak = $"{toSpeak} \n Value: {currentEyeColorValue}";
}
else
{
prevEyeColorValue = "";
}
}
if (prevEyeColor != currentEyeColor)
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 522 || currentComponent.myID <= 524)))
{
prevEyeColor = currentEyeColor;
if (currentEyeColor != "")
toSpeak = $"{toSpeak} \n {currentEyeColor}";
}
}
if (prevHairColorHue != currentHairColorHue)
{
if (currentComponent != null && currentComponent.myID == 525)
{
prevHairColorHue = currentHairColorHue;
if (currentHairColorHue != "")
toSpeak = $"{toSpeak} \n Hue: {currentHairColorHue}";
}
else
{
prevHairColorHue = "";
}
}
if (prevHairColorSaturation != currentHairColorSaturation)
{
if (currentComponent != null && currentComponent.myID == 526)
{
prevHairColorSaturation = currentHairColorSaturation;
if (currentHairColorSaturation != "")
toSpeak = $"{toSpeak} \n Saturation: {currentHairColorSaturation}";
}
else
{
prevHairColorSaturation = "";
}
}
if (prevHairColorValue != currentHairColorValue)
{
if (currentComponent != null && currentComponent.myID == 527)
{
prevHairColorValue = currentHairColorValue;
if (currentHairColorValue != "")
toSpeak = $"{toSpeak} \n Value: {currentHairColorValue}";
}
else
{
prevHairColorValue = "";
}
}
if (prevHairColor != currentHairColor)
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 525 || currentComponent.myID <= 527)))
{
prevHairColor = currentHairColor;
if (currentHairColor != "")
toSpeak = $"{toSpeak} \n {currentHairColor}";
}
}
if (prevPantsColorHue != currentPantsColorHue)
{
if (currentComponent != null && currentComponent.myID == 528)
{
prevPantsColorHue = currentPantsColorHue;
if (currentPantsColorHue != "")
toSpeak = $"{toSpeak} \n Hue: {currentPantsColorHue}";
}
else
{
prevPantsColorHue = "";
}
}
if (prevPantsColorSaturation != currentPantsColorSaturation)
{
if (currentComponent != null && currentComponent.myID == 529)
{
prevPantsColorSaturation = currentPantsColorSaturation;
if (currentPantsColorSaturation != "")
toSpeak = $"{toSpeak} \n Saturation: {currentPantsColorSaturation}";
}
else
{
prevPantsColorSaturation = "";
}
}
if (prevPantsColorValue != currentPantsColorValue)
{
if (currentComponent != null && currentComponent.myID == 530)
{
prevPantsColorValue = currentPantsColorValue;
if (currentPantsColorValue != "")
toSpeak = $"{toSpeak} \n Value: {currentPantsColorValue}";
}
else
{
prevPantsColorValue = "";
}
}
if (prevPantsColor != currentPantsColor)
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 528 || currentComponent.myID <= 530)))
{
prevPantsColor = currentPantsColor;
if (currentPantsColor != "")
toSpeak = $"{toSpeak} \n {currentPantsColor}";
}
}
}
if (prevPet != currentPet)
{
prevPet = currentPet;
if (currentPet != "")
toSpeak = $"{toSpeak} \n Current Pet: {currentPet}";
}
return toSpeak.Trim();
}
private static string CycleThroughItems(bool increase, CharacterCustomization __instance, bool ___skipIntro,
ClickableComponent ___startingCabinsLabel, ClickableComponent ___difficultyModifierLabel, TextBox ___nameBox,
TextBox ___farmnameBox, TextBox ___favThingBox)
{
string toSpeak = " ";
int DesignControlsIndex = 0;
Dictionary<ClickableComponent, string> buttons = new();
#region Add buttons with their names IF they are available
#region Character related
string postText = "";
if (__instance.nameBoxCC != null && __instance.nameBoxCC.visible)
{
if (___nameBox.Text != "")
{
postText = $": {___nameBox.Text}";
}
else
{
postText = " Text Box";
}
buttons.Add(__instance.nameBoxCC, $"Farmer's Name{postText}");
}
if (__instance.farmnameBoxCC != null && __instance.farmnameBoxCC.visible)
{
if (___farmnameBox.Text != "")
{
postText = $": {___farmnameBox.Text}";
}
else
{
postText = " Text Box";
}
buttons.Add(__instance.farmnameBoxCC, $"Farm's Name{postText}");
}
if (__instance.favThingBoxCC != null && __instance.favThingBoxCC.visible)
{
if (___favThingBox.Text != "")
{
postText = $": {___favThingBox.Text}";
}
else
{
postText = " Text Box";
}
buttons.Add(__instance.favThingBoxCC, $"Favourite Thing{postText}");
}
if (__instance.petPortraitBox.HasValue) // Cannot get petButtons like with others
{
ClickableComponent petPrev = __instance.getComponentWithID(511);
buttons.Add(petPrev, "Previous pet button");
ClickableComponent petNext = __instance.getComponentWithID(510);
buttons.Add(petNext, "Next pet button");
}
if (__instance.randomButton != null && __instance.randomButton.visible)
buttons.Add(__instance.randomButton, "Random Skin Button");
// Controls to rotate the farmer (Potentially useful for low vision players) are first if they're available.
// They also appear above the gender buttons, so we handle them separately here.
if (characterDesignToggle && new[] { __instance.leftSelectionButtons.Count, __instance.rightSelectionButtons.Count }.All(c => c >= 0)) // both have Count > 0
{
if (new[] { __instance.leftSelectionButtons[DesignControlsIndex].visible, __instance.rightSelectionButtons[DesignControlsIndex].visible }.All(v => v == true) // both visible
&& new[] { __instance.leftSelectionButtons[DesignControlsIndex].name, __instance.rightSelectionButtons[DesignControlsIndex].name }.All(n => n == "Direction")) // both named "Direction"
{
buttons.Add(__instance.leftSelectionButtons[DesignControlsIndex], "Rotate Left Button");
buttons.Add(__instance.rightSelectionButtons[DesignControlsIndex], "Rotate Right Button");
++DesignControlsIndex;
}
}
if (__instance.genderButtons.Count > 0)
{
buttons.Add(__instance.genderButtons[0], ((Game1.player.IsMale) ? "Selected " : "") + "Gender: Male Button");
buttons.Add(__instance.genderButtons[1], ((!Game1.player.IsMale) ? "Selected " : "") + "Gender: Female Button");
}
if (characterDesignToggle && new[] { __instance.leftSelectionButtons.Count, __instance.rightSelectionButtons.Count }.All(c => c >= DesignControlsIndex) && new[] { __instance.leftSelectionButtons[DesignControlsIndex].visible, __instance.rightSelectionButtons[DesignControlsIndex].visible }.All(v => v == true))
{
while (DesignControlsIndex < __instance.leftSelectionButtons.Count)
{
ClickableComponent left = __instance.leftSelectionButtons[DesignControlsIndex];
ClickableComponent right = __instance.rightSelectionButtons[DesignControlsIndex];
string name = left.name;
// minor cleanup on names to be slightly more descriptive
switch (name)
{
case "Skin":
name += " Tone";
break;
case "Hair":
name += " Style";
break;
case "Acc":
name = "Accessory";
break;
default:
break;
}
if (!buttons.ContainsKey(left) || !buttons.ContainsKey(right))
{
buttons.Add(left, $"Previous {name} button");
buttons.Add(right, $"Next {name} button");
}
//MainClass.ScreenReader.Say($"Left {DesignControlsIndex}: {__instance.leftSelectionButtons[DesignControlsIndex]} {__instance.leftSelectionButtons[DesignControlsIndex].name}\n", true);
//MainClass.ScreenReader.Say($"Right {DesignControlsIndex}: {__instance.rightSelectionButtons[DesignControlsIndex]} {__instance.rightSelectionButtons[DesignControlsIndex].name}\n", true);
++DesignControlsIndex;
}
ClickableComponent eyeColorHue = __instance.getComponentWithID(522);
if (eyeColorHue != null && eyeColorHue.visible)
buttons.Add(eyeColorHue, "eye color hue slider");
ClickableComponent eyeColorSaturation = __instance.getComponentWithID(523);
if (eyeColorSaturation != null && eyeColorSaturation.visible)
buttons.Add(eyeColorSaturation, "eye color saturation slider");
ClickableComponent eyeColorValue = __instance.getComponentWithID(524);
if (eyeColorValue != null && eyeColorValue.visible)
buttons.Add(eyeColorValue, "eye color Value slider");
ClickableComponent hairColorHue = __instance.getComponentWithID(525);
if (hairColorHue != null && hairColorHue.visible)
buttons.Add(hairColorHue, "hair color hue slider");
ClickableComponent hairColorSaturation = __instance.getComponentWithID(526);
if (hairColorSaturation != null && hairColorSaturation.visible)
buttons.Add(hairColorSaturation, "hair color saturation slider");
ClickableComponent hairColorValue = __instance.getComponentWithID(527);
if (hairColorValue != null && hairColorValue.visible)
buttons.Add(hairColorValue, "hair color Value slider");
ClickableComponent pantsColorHue = __instance.getComponentWithID(528);
if (pantsColorHue != null && pantsColorHue.visible)
buttons.Add(pantsColorHue, "pants color hue slider");
ClickableComponent pantsColorSaturation = __instance.getComponentWithID(529);
if (pantsColorSaturation != null && pantsColorSaturation.visible)
buttons.Add(pantsColorSaturation, "pants color saturation slider");
ClickableComponent pantsColorValue = __instance.getComponentWithID(530);
if (pantsColorValue != null && pantsColorValue.visible)
buttons.Add(pantsColorValue, "pants color Value slider");
}
#endregion
#region Farm layout related
if (__instance.farmTypeButtons.Count > 0)
{
for (int i = 0; i < __instance.farmTypeButtons.Count; i++)
{
buttons.Add(__instance.farmTypeButtons[i], ((i == Game1.whichFarm) ? "Selected " : "") + getFarmHoverText(__instance.farmTypeButtons[i]));
}
}
if (__instance.farmTypeNextPageButton != null && __instance.farmTypeNextPageButton.visible)
buttons.Add(__instance.farmTypeNextPageButton, "Next Farm Type Page Button");
if (__instance.farmTypePreviousPageButton != null && __instance.farmTypePreviousPageButton.visible)
buttons.Add(__instance.farmTypePreviousPageButton, "Previous Farm Type Page Button");
#endregion
#region Co-op related
if (__instance.source == CharacterCustomization.Source.HostNewFarm)
{
ClickableComponent cabinLeft = __instance.getComponentWithID(621);
if (Game1.startingCabins > 0)
buttons.Add(cabinLeft, "Decrease starting cabins button");
buttons.Add(___startingCabinsLabel, $"Starting cabins: {Game1.startingCabins}");
ClickableComponent cabinRight = __instance.getComponentWithID(622);
if (Game1.startingCabins < 3)
buttons.Add(cabinRight, "Increase starting cabins button");
if (Game1.startingCabins > 0)
{
buttons.Add(__instance.cabinLayoutButtons[0], "Cabin layout to nearby Button");
buttons.Add(__instance.cabinLayoutButtons[1], "Cabin layout to separate Button");
}
ClickableComponent difficultyLeft = __instance.getComponentWithID(627);
buttons.Add(difficultyLeft, "Increase profit margin button");
buttons.Add(___difficultyModifierLabel, "Profit Margin: " + (((Game1.player.difficultyModifier * 100) == 100f) ? "normal" : Game1.player.difficultyModifier.ToString()));
ClickableComponent difficultyRight = __instance.getComponentWithID(628);
buttons.Add(difficultyRight, "Decrease profit margin button");
ClickableComponent walletLeft = __instance.getComponentWithID(631);
buttons.Add(walletLeft, "Money style to " + ((!Game1.player.team.useSeparateWallets.Value) ? "separate wallets" : "shared wallets") + " button");
}
#endregion
if (__instance.skipIntroButton != null && __instance.skipIntroButton.visible)
buttons.Add(__instance.skipIntroButton, (___skipIntro ? "Enabled" : "Disabled") + " Skip Intro Button");
if (__instance.advancedOptionsButton != null && __instance.advancedOptionsButton.visible)
buttons.Add(__instance.advancedOptionsButton, "Advanced Options Button");
if (__instance.okButton != null && __instance.okButton.visible)
buttons.Add(__instance.okButton, "OK Button");
if (__instance.backButton != null && __instance.backButton.visible)
buttons.Add(__instance.backButton, "Back Button");
#endregion
int size = buttons.Count - 1;
if (increase)
{
saveGameIndex++;
if (saveGameIndex > size)
saveGameIndex = 0;
}
else
{
saveGameIndex--;
if (saveGameIndex < 0)
saveGameIndex = size;
}
currentComponent = buttons.ElementAt(saveGameIndex).Key;
currentComponent!.snapMouseCursor();
__instance.setCurrentlySnappedComponentTo(currentComponent!.myID);
toSpeak = buttons.ElementAt(saveGameIndex).Value;
return toSpeak.Trim();
}
private static string getFarmHoverText(ClickableTextureComponent farm)
{
string hoverTitle = " ", hoverText = " ";
if (!farm.name.Contains("Gray"))
{
if (farm.hoverText.Contains('_'))
{
hoverTitle = farm.hoverText.Split('_')[0];
hoverText = farm.hoverText.Split('_')[1];
}
else
{
hoverTitle = " ";
hoverText = farm.hoverText;
}
}
else
{
if (farm.name.Contains("Gray"))
{
hoverText = "Reach level 10 " + Game1.content.LoadString("Strings\\UI:Character_" + farm.name.Split('_')[1]) + " to unlock.";
}
}
return $"{hoverTitle}: {hoverText}";
}
private static SliderBar? getCurrentSliderBar(int id, CharacterCustomization __instance)
{
if (id >= 522 && id <= 530)
{
// Three ColorPickers with 3 SliderBars each.
// First group ids by ColorPicker.
// Maps 522-524 -> 0, 525-527 -> 1, 528-530 -> 2
int whichColorPicker = (int)Math.Floor(((float)id - 522f) / 3f);
// Next group ids by slider type.
// Maps [522,525,528] -> 0, [523,526,529] -> 1, [524,527,530] -> 2
int whichSliderBar = (int)Math.Floor((float)id % 3f);
ColorPicker cp;
switch (whichColorPicker)
{
default:
case 0:
// 522-524 == eye color
cp = __instance.eyeColorPicker;
break;
case 1:
// 525-527 == hair color
cp = __instance.hairColorPicker;
break;
case 2:
// 528-530 == pants color
cp = __instance.pantsColorPicker;
break;
}
SliderBar sb;
switch (whichSliderBar)
{
default:
case 0:
// 522, 525, 528 == hue slider
sb = cp.hueBar;
break;
case 1:
// 523, 526, 529 == saturation slider
sb = cp.saturationBar;
break;
case 2:
// 524, 527, 530 == value slider
sb = cp.valueBar;
break;
}
return sb;
}
else
{
return null;
}
}
private static void AdjustCurrentSlider(bool increase, CharacterCustomization __instance, int amount = 1)
{
if (currentComponent != null && currentComponent.myID >= 522 && currentComponent.myID <= 530)
{
SliderBar sb = getCurrentSliderBar(currentComponent.myID, __instance)!;
if (sb != null)
{
double step = ((double)sb.bounds.Width / 100d); // size of 1% change in slider value
double value = (double)sb.value;
double x = 0d;
int y = currentComponent.bounds.Center.Y;
if (increase)
{
value = Math.Min(value + amount, 99d);
x = Math.Min(Math.Ceiling((value * step)), (double)sb.bounds.Width);
}
else
{
value = Math.Max(value - amount, 0d);
x = Math.Max(Math.Ceiling((value * step)), 0d);
}
x += (double)currentComponent.bounds.Left;
Game1.setMousePosition((int)x, y);
Game1.activeClickableMenu.receiveLeftClick((int)x, y);
}
}
}
// Most values (exception noted below) are 0 indexed internally but visually start from 1. Thus we increment before returning.
private static string GetCurrentPet(bool lessInfo = false)
{
if (currentComponent != null && currentComponent.name == "Pet")
{
int whichPetBreed = Game1.player.whichPetBreed + 1;
if (!lessInfo)
{
string petType = Game1.player.catPerson ? "Cat" : "Dog";
if (descriptions.TryGetValue(petType, out var innerDict) && innerDict.TryGetValue(whichPetBreed, out var description))
{
return description;
}
else
{
MainClass.ErrorLog($"Warning: Description for {petType} with index {whichPetBreed} not found in the dictionary.");
}
}
return $"{(Game1.player.catPerson ? "Cat" : "Dog")} #{whichPetBreed + 1}";
}
return "";
}
private static string GetCurrentAttributeValue(string componentName, Func<int> getValue, bool lessInfo = false)
{
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == componentName))
{
int index = getValue();
if (!lessInfo)
{
if (descriptions.TryGetValue(componentName, out var innerDict))
{
if (innerDict.TryGetValue(index, out var description))
{
return description;
}
else
{
MainClass.ErrorLog($"Warning: Description for {componentName} with index {index} not found in the inner dictionary.");
}
}
else
{
MainClass.ErrorLog($"Warning: Description for {componentName} not found in the outer dictionary.");
}
}
return $"{componentName}: {index}";
}
return "";
}
private static string GetCurrentSkin(bool lessInfo = false) => GetCurrentAttributeValue("Skin", () => Game1.player.skin.Value + 1, lessInfo);
private static string GetCurrentHair(bool lessInfo = false) => GetCurrentAttributeValue("Hair", () => Game1.player.hair.Value + 1, lessInfo);
private static string GetCurrentShirt(bool lessInfo = false) => GetCurrentAttributeValue("Shirt", () => Game1.player.shirt.Value + 1, lessInfo);
private static string GetCurrentPants(bool lessInfo = false) => GetCurrentAttributeValue("Pants Style", () => Game1.player.pants.Value + 1, lessInfo);
private static string GetCurrentAccessory(bool lessInfo = false) => GetCurrentAttributeValue("Accessory", () => Game1.player.accessory.Value + 2, lessInfo);
private static string GetCurrentColorAttributeValue(string componentName, int minID, int maxID, Func<string> getValue)
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= minID && currentComponent.myID <= maxID)))
{
return $"{componentName}: {getValue()}";
}
return "";
}
private static string GetCurrentEyeColor() => GetCurrentColorAttributeValue("Eye color", 522, 524, () => $"{Game1.player.newEyeColor.R}, {Game1.player.newEyeColor.G}, {Game1.player.newEyeColor.B}");
private static string GetCurrentEyeColorHue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Eye color hue", 522, 524, () => (getCurrentSliderBar(522, __instance)!.value!.ToString()));
private static string GetCurrentEyeColorSaturation(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Eye color saturation", 522, 524, () => (getCurrentSliderBar(523, __instance)!.value!.ToString()));
private static string GetCurrentEyeColorValue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Eye color value", 522, 524, () => (getCurrentSliderBar(524, __instance)!.value!.ToString()));
private static string GetCurrentHairColor() => GetCurrentColorAttributeValue("Hair color", 525, 527, () => $"{Game1.player.hairstyleColor.R}, {Game1.player.hairstyleColor.G}, {Game1.player.hairstyleColor.B}");
private static string GetCurrentHairColorHue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Hair color hue", 525, 527, () => (getCurrentSliderBar(525, __instance)!.value!.ToString()));
private static string GetCurrentHairColorSaturation(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Hair color saturation", 525, 527, () => (getCurrentSliderBar(526, __instance)!.value!.ToString()));
private static string GetCurrentHairColorValue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Hair color value", 525, 527, () => (getCurrentSliderBar(527, __instance)!.value!.ToString()));
private static string GetCurrentPantsColor() => GetCurrentColorAttributeValue("Pants color", 528, 530, () => $"{Game1.player.pantsColor.R}, {Game1.player.pantsColor.G}, {Game1.player.pantsColor.B}");
private static string GetCurrentPantsColorHue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Pants color hue", 528, 530, () => (getCurrentSliderBar(528, __instance)!.value!.ToString()));
private static string GetCurrentPantsColorSaturation(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Pants color saturation", 528, 530, () => (getCurrentSliderBar(529, __instance)!.value!.ToString()));
private static string GetCurrentPantsColorValue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Pants color value", 528, 530, () => (getCurrentSliderBar(530, __instance)!.value!.ToString()));
}
}

View File

@@ -0,0 +1,59 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class CoopMenuPatch
{
private static string coopMenuQueryKey = "";
internal static void DrawPatch(CoopMenu __instance, CoopMenu.Tab ___currentTab)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true);
string toSpeak = "";
#region Join/Host Button (Important! This should be checked before checking other buttons)
if (__instance.slotButtons[0].containsPoint(x, y))
{
if (___currentTab == CoopMenu.Tab.JOIN_TAB)
toSpeak = "Join lan game";
if (___currentTab == CoopMenu.Tab.HOST_TAB)
toSpeak = "Host new farm";
}
#endregion
#region Other Buttons
if (__instance.joinTab.containsPoint(x, y))
{
toSpeak = "Join Tab Button";
}
else if (__instance.hostTab.containsPoint(x, y))
{
toSpeak = "Host Tab Button";
}
else if (__instance.refreshButton.containsPoint(x, y))
{
toSpeak = "Refresh Button";
}
#endregion
if (coopMenuQueryKey != toSpeak)
{
coopMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in co-op menu patch:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
coopMenuQueryKey = "";
}
}
}

View File

@@ -0,0 +1,86 @@
using StardewValley;
using StardewValley.Menus;
using static StardewValley.Menus.LoadGameMenu;
namespace stardew_access.Patches
{
internal class LoadGameMenuPatch
{
private static string loadGameMenuQueryKey = "";
private static bool firstTimeInMenu = true;
internal static void DrawPatch(SaveFileSlot __instance, LoadGameMenu ___menu, int i)
{
try
{
int x = Game1.getMouseX(true), y = Game1.getMouseY(true);
string toSpeak = "";
if (!___menu.slotButtons[i].containsPoint(x, y)) return;
if (__instance.Farmer == null) return;
if (___menu.deleteButtons.Count > 0 && ___menu.deleteButtons[i].containsPoint(x, y))
{
toSpeak = $"Delete {__instance.Farmer.farmName.Value} Farm";
if (loadGameMenuQueryKey != toSpeak)
{
loadGameMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
if (___menu.deleteConfirmationScreen)
{
if (firstTimeInMenu)
{
firstTimeInMenu = false;
toSpeak = "Really delete farm?";
}
if (___menu.okDeleteButton.containsPoint(x, y))
{
toSpeak = $"{toSpeak} Ok button";
}
else if (___menu.cancelDeleteButton.containsPoint(x, y))
{
toSpeak = $"{toSpeak} Cancel button";
}
if (loadGameMenuQueryKey != toSpeak)
{
loadGameMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
return;
}
String farmerName = __instance.Farmer.displayName;
String farmName = __instance.Farmer.farmName.Value;
String money = __instance.Farmer.Money.ToString();
String hoursPlayed = Utility.getHoursMinutesStringFromMilliseconds(__instance.Farmer.millisecondsPlayed);
string dateStringForSaveGame = ((!__instance.Farmer.dayOfMonthForSaveGame.HasValue ||
!__instance.Farmer.seasonForSaveGame.HasValue ||
!__instance.Farmer.yearForSaveGame.HasValue) ? __instance.Farmer.dateStringForSaveGame : Utility.getDateStringFor(__instance.Farmer.dayOfMonthForSaveGame.Value, __instance.Farmer.seasonForSaveGame.Value, __instance.Farmer.yearForSaveGame.Value));
toSpeak = $"{farmName} Farm Selected, \t\n Farmer: {farmerName}, \t\nMoney: {money}, \t\nHours Played: {hoursPlayed}, \t\nDate: {dateStringForSaveGame}";
if (loadGameMenuQueryKey != toSpeak)
{
loadGameMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in load game menu patch:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
loadGameMenuQueryKey = "";
firstTimeInMenu = true;
}
}
}

View File

@@ -0,0 +1,81 @@
using StardewValley;
using StardewValley.Menus;
namespace stardew_access.Patches
{
internal class TitleMenuPatch
{
private static string titleMenuQueryKey = "";
internal static void DrawPatch(TitleMenu __instance, bool ___isTransitioningButtons)
{
try
{
if (___isTransitioningButtons)
return;
int x = Game1.getMouseX(true), y = Game1.getMouseY(true); // Mouse x and y position
string toSpeak = "";
if (__instance.muteMusicButton.containsPoint(x, y))
{
toSpeak = "Mute Music Button";
}
else if (__instance.aboutButton.containsPoint(x, y))
{
toSpeak = "About Button";
}
else if (__instance.languageButton.containsPoint(x, y))
{
toSpeak = "Language Button";
}
else if (__instance.windowedButton.containsPoint(x, y))
{
toSpeak = "Fullscreen: " + ((Game1.isFullscreen) ? "enabled" : "disabled");
}
else if (TitleMenu.subMenu != null && __instance.backButton.containsPoint(x, y))
{
string text = "Back Button";
MainClass.ScreenReader.SayWithChecker(text, true);
}
else
{
__instance.buttons.ForEach(component =>
{
if (!component.containsPoint(Game1.getMouseX(true), Game1.getMouseY(true)))
return;
string name = component.name;
string label = component.label;
toSpeak = $"{name} {label} Button";
});
}
// Fix for back button not working using keyboard
if (TitleMenu.subMenu is CharacterCustomization && ((CharacterCustomization)TitleMenu.subMenu).backButton.containsPoint(x, y))
{
// Perform Left Click
if (MainClass.Config.LeftClickMainKey.JustPressed())
{
__instance.backButtonPressed();
}
}
if (TitleMenu.subMenu == null && titleMenuQueryKey!=toSpeak)
{
titleMenuQueryKey = toSpeak;
MainClass.ScreenReader.Say(toSpeak, true);
}
}
catch (Exception e)
{
MainClass.ErrorLog($"An error occured in title menu patch:\n{e.Message}\n{e.StackTrace}");
}
}
internal static void Cleanup()
{
titleMenuQueryKey = "";
}
}
}

View File

@@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
public static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetDllDirectory(string lpPathName);
}

View File

@@ -6,16 +6,9 @@ namespace stardew_access.ScreenReader
{ {
public IScreenReader Initialize() public IScreenReader Initialize()
{ {
IScreenReader ScreenReader = new ScreenReaderWindows(); // Default is windows IScreenReader ScreenReader = new ScreenReaderMac(); // Mac by default
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
ScreenReaderWindows screenReaderWindows = new ScreenReaderWindows();
screenReaderWindows.InitializeScreenReader();
ScreenReader = screenReaderWindows;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{ {
ScreenReaderLinux screenReaderLinux = new ScreenReaderLinux(); ScreenReaderLinux screenReaderLinux = new ScreenReaderLinux();
screenReaderLinux.InitializeScreenReader(); screenReaderLinux.InitializeScreenReader();

View File

@@ -1,3 +1,8 @@
/*
Linux speech dispatcher library used:
https://github.com/shoaib11120/libspeechdwrapper
*/
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace stardew_access.ScreenReader namespace stardew_access.ScreenReader
@@ -36,10 +41,16 @@ namespace stardew_access.ScreenReader
public void InitializeScreenReader() public void InitializeScreenReader()
{ {
MainClass.InfoLog("Initializing speech dispatcher...");
int res = Initialize(); int res = Initialize();
if (res == 1) if (res == 1)
{ {
initialized = true; initialized = true;
MainClass.InfoLog("Successfully initialized.");
}
else
{
MainClass.ErrorLog("Unable to initialize.");
} }
} }
@@ -57,10 +68,24 @@ namespace stardew_access.ScreenReader
if (text == null) if (text == null)
return; return;
if (initialized) if (!initialized)
return;
if (!MainClass.Config.TTS)
return;
if (text.Contains('^')) text = text.Replace('^', '\n');
GoString str = new GoString(text, text.Length);
int re = Speak(str, interrupt);
if (re == 1)
{ {
GoString str = new GoString(text, text.Length); MainClass.DebugLog($"Speaking(interrupt: {interrupt}) = {text}");
Speak(str, interrupt); }
else
{
MainClass.ErrorLog($"Failed to output text: {text}");
} }
} }

View File

@@ -1,53 +1,179 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using static System.Net.Mime.MediaTypeNames;
namespace stardew_access.ScreenReader namespace stardew_access.ScreenReader
{ {
public class ScreenReaderMac : IScreenReader public class ScreenReaderMac : IScreenReader
{ {
private Process? _speakProcess; // The speaker instance
private static IntPtr speaker;
//Stuff for the runloop thread
private CancellationTokenSource cts = new CancellationTokenSource();
private Thread rt;
//Speech queue for interrupt
private static Queue<string> speechQueue = new Queue<string>();
// DidFinishSpeaking callback for interrupt
dfs_callback fscb = new dfs_callback(DoneSpeaking);
// Dylib imports
///////////
// Speaker
//
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void init_speaker();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void speak(string text);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_voice(Int32 index);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern UInt32 available_voices_count();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_language(Int32 index);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern UInt32 available_languages_count();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void get_voice_name(UInt32 idx, String pszOut);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_volume(Single volume);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern Single get_volume();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_rate(Single rate);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern Single get_rate();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void stop();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void cleanup_speaker();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void mainloop_speaker(IntPtr speaker);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern Boolean is_speaking(IntPtr speaker);
///////////////
// Speaker OO
//
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr make_speaker();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void speak_with(IntPtr speaker, String text);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_voice_with(IntPtr speaker, Int32 index);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_volume_with(IntPtr speaker, Single volume);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern Single get_volume_with(IntPtr speaker);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void set_rate_with(IntPtr speaker, Single rate);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern Single get_rate_with(IntPtr speaker);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void stop_with(IntPtr speaker);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void cleanup_with(IntPtr speaker);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void wsw_callback(String p1);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void wsp_callback(Int16 p1);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void dfs_callback();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void register_will_speak_word_callback(IntPtr speaker, [MarshalAs(UnmanagedType.FunctionPtr)]wsw_callback cb);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void register_will_speak_phoneme_callback(IntPtr speaker, [MarshalAs(UnmanagedType.FunctionPtr)]wsp_callback cb);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void register_did_finish_speaking_callback(IntPtr speaker, [MarshalAs(UnmanagedType.FunctionPtr)]dfs_callback cb);
/////////////////
// Recognizer OO
//
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr make_listener();
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void start_listening(IntPtr listener);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void stop_listening(IntPtr listener);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void add_command(IntPtr listener, String command);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void cleanup_listener(IntPtr listener);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void mainloop_listener(IntPtr listener);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern Boolean is_listening(IntPtr listener);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void drc_callback(String p1);
[DllImport("libspeak", CallingConvention = CallingConvention.Cdecl)]
private static extern void register_did_recognize_command_callback(IntPtr listener, drc_callback cb);
public string PrevTextTile public string PrevTextTile
{ {
get; get { return prevTextTile; }
set; set { prevTextTile = value; }
} }
public string prevText = "", prevTextTile = " ", prevChatText = "", prevMenuText = ""; public string prevText = "", prevTextTile = " ", prevChatText = "", prevMenuText = "";
public void InitializeScreenReader() private static void SpeakLoop(object obj)
{ {
MainClass.GetMonitor().Log("Screen reader initialized"); CancellationToken ct = (CancellationToken)obj;
_speakProcess = new Process();
_speakProcess.StartInfo.FileName = "mac"; while (!ct.IsCancellationRequested)
_speakProcess.StartInfo.Verb = "mac"; {
_speakProcess.StartInfo.RedirectStandardInput = true; mainloop_speaker(speaker);
_speakProcess.Start(); Thread.Sleep(20);
Speak("Mac screen reader ready", true); }
} }
public void InitializeScreenReader()
{
MainClass.InfoLog("Screen reader initialized");
speaker = make_speaker();
rt = new Thread(new ParameterizedThreadStart(SpeakLoop));
rt.Start(cts.Token);
register_did_finish_speaking_callback(speaker, fscb);
set_rate_with(speaker, MainClass.Config.SpeechRate);
Say("Mac screen reader ready", true);
}
public void CloseScreenReader() public void CloseScreenReader()
{ {
MainClass.GetMonitor().Log("Screen reader closed"); cts.Cancel();
_speakProcess.Kill(); rt.Join();
cts.Dispose();
cleanup_with(speaker);
} }
public void Say(string text, bool interrupt) public void Say(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}"); if (interrupt)
Speak(text, interrupt); {
speechQueue.Clear();
speak_with(speaker,text);
} else {
speechQueue.Enqueue(text);
}
} }
public void SayWithChecker(string text, bool interrupt) public void SayWithChecker(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}");
if (text != prevText) if (text != prevText)
{ {
Speak(text, interrupt); MainClass.InfoLog($"{text}");
Say(text, interrupt);
prevText = text; prevText = text;
} }
} }
@@ -55,10 +181,10 @@ namespace stardew_access.ScreenReader
public void SayWithMenuChecker(string text, bool interrupt) public void SayWithMenuChecker(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}");
if (text != prevMenuText) if (text != prevMenuText)
{ {
Speak(text, interrupt); MainClass.InfoLog($"{text}");
Say(text, interrupt);
prevMenuText = text; prevMenuText = text;
} }
} }
@@ -66,10 +192,10 @@ namespace stardew_access.ScreenReader
public void SayWithChatChecker(string text, bool interrupt) public void SayWithChatChecker(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}");
if (text != prevChatText) if (text != prevChatText)
{ {
Speak(text, interrupt); MainClass.InfoLog($"{text}");
Say(text, interrupt);
prevChatText = text; prevChatText = text;
} }
} }
@@ -77,29 +203,21 @@ namespace stardew_access.ScreenReader
public void SayWithTileQuery(string text, int x, int y, bool interrupt) public void SayWithTileQuery(string text, int x, int y, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}");
if (text != prevTextTile) if (text != prevTextTile)
{ {
Speak(text, interrupt); MainClass.InfoLog($"{text}");
Say(text, interrupt);
prevTextTile = text; prevTextTile = text;
}
} }
} private static void DoneSpeaking()
private void Speak(string text, bool interupt)
{ {
if (_speakProcess.HasExited) { if (speechQueue.Count != 0)
InitializeScreenReader();
}
if (interupt)
{ {
_speakProcess.StandardInput.WriteLine("x"); speak_with(speaker, speechQueue.Dequeue());
}
var lines = text.Split("\n").ToArray();
foreach (var line in lines) {
_speakProcess.StandardInput.WriteLine($"s{line}");
} }
}
}
} }
} }

View File

@@ -19,26 +19,26 @@ namespace stardew_access.ScreenReader
public void InitializeScreenReader() public void InitializeScreenReader()
{ {
MainClass.GetMonitor().Log("Screen reader initialized"); MainClass.InfoLog("Screen reader initialized");
Speak("Mac screen reader ready", true); Speak("Mac screen reader ready", true);
} }
public void CloseScreenReader() public void CloseScreenReader()
{ {
MainClass.GetMonitor().Log("Screen reader closed"); MainClass.InfoLog("Screen reader closed");
} }
public void Say(string text, bool interrupt) public void Say(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}"); MainClass.InfoLog($"{text}");
Speak(text, interrupt); Speak(text, interrupt);
} }
public void SayWithChecker(string text, bool interrupt) public void SayWithChecker(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}"); MainClass.InfoLog($"{text}");
if (text != prevText) { if (text != prevText) {
Speak(text, interrupt); Speak(text, interrupt);
prevText = text; prevText = text;
@@ -48,7 +48,7 @@ namespace stardew_access.ScreenReader
public void SayWithMenuChecker(string text, bool interrupt) public void SayWithMenuChecker(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}"); MainClass.InfoLog($"{text}");
if (text != prevMenuText) { if (text != prevMenuText) {
Speak(text, interrupt); Speak(text, interrupt);
prevMenuText = text; prevMenuText = text;
@@ -58,7 +58,7 @@ namespace stardew_access.ScreenReader
public void SayWithChatChecker(string text, bool interrupt) public void SayWithChatChecker(string text, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}"); MainClass.InfoLog($"{text}");
if (text != prevChatText) { if (text != prevChatText) {
Speak(text, interrupt); Speak(text, interrupt);
prevChatText = text; prevChatText = text;
@@ -68,7 +68,7 @@ namespace stardew_access.ScreenReader
public void SayWithTileQuery(string text, int x, int y, bool interrupt) public void SayWithTileQuery(string text, int x, int y, bool interrupt)
{ {
if (text == null) return; if (text == null) return;
MainClass.GetMonitor().Log($"{text}"); MainClass.InfoLog($"{text}");
if (text != prevTextTile) { if (text != prevTextTile) {
Speak(text, interrupt); Speak(text, interrupt);
prevTextTile = text; prevTextTile = text;

View File

@@ -1,106 +0,0 @@
using AccessibleOutput;
namespace stardew_access.ScreenReader
{
public class ScreenReaderWindows : IScreenReader
{
public IAccessibleOutput? screenReader = null;
public string prevText = "", prevTextTile = " ", prevChatText = "", prevMenuText = "";
public string PrevTextTile
{
get { return prevTextTile; }
set { prevTextTile = value; }
}
public void InitializeScreenReader()
{
NvdaOutput? nvdaOutput = null;
JawsOutput? jawsOutput = null;
SapiOutput? sapiOutput = null;
// Initialize NVDA
try
{
nvdaOutput = new NvdaOutput();
}
catch (Exception) { }
// Initialize JAWS
try
{
jawsOutput = new JawsOutput();
}
catch (Exception) { }
// Initialize SAPI
try
{
sapiOutput = new SapiOutput();
}
catch (Exception) { }
if (nvdaOutput != null && nvdaOutput.IsAvailable())
screenReader = nvdaOutput;
else if (jawsOutput != null && jawsOutput.IsAvailable())
screenReader = jawsOutput;
else if (sapiOutput != null && sapiOutput.IsAvailable())
screenReader = sapiOutput;
}
public void CloseScreenReader()
{
}
public void Say(string text, bool interrupt)
{
if (text == null)
return;
if (screenReader == null)
return;
screenReader.Speak(text, interrupt);
}
public void SayWithChecker(string text, bool interrupt)
{
if (prevText != text)
{
prevText = text;
Say(text, interrupt);
}
}
public void SayWithMenuChecker(string text, bool interrupt)
{
if (prevMenuText != text)
{
prevMenuText = text;
Say(text, interrupt);
}
}
public void SayWithChatChecker(string text, bool interrupt)
{
if (prevChatText != text)
{
prevChatText = text;
Say(text, interrupt);
}
}
public void SayWithTileQuery(string text, int x, int y, bool interrupt)
{
string query = $"{text} x:{x} y:{y}";
if (prevTextTile != query)
{
prevTextTile = query;
Say(text, interrupt);
}
}
}
}

View File

@@ -0,0 +1,31 @@
{
"Egg Festival": {
"21,55": "Egg Festival Shop"
},
"Flower Dance": {
"28,37": "Flower Dance Shop"
},
"Luau": {
"35,13": "Soup Pot"
},
"Spirit's Eve": {
"25,49": "Spirit's Eve Shop"
},
"Stardew Valley Fair": {
"16,52": "Stardew Valley Fair Shop",
"23,62": "Slingshot Game",
"34,65": "Purchase Star Tokens",
"33,70": "The Wheel",
"23,70": "Fishing Challenge",
"47,87": "Fortune Teller",
"38,59": "Grange Display",
"30,56": "Strength Game",
"26,33": "Free Burgers"
},
"Festival of Ice": {
"55,31": "Travelling Cart"
},
"Feast of the Winter Star": {
"18,61": "Feast of the Winter Star Shop"
}
}

View File

@@ -0,0 +1,256 @@
{
"Cat": {
"1": "Orange cat",
"2": "Gray Tabby cat with white belly",
"3": "Yellow cat with purple collar"
},
"Dog": {
"1": "Golden-brown Hound with blue collar",
"2": "Brown Shepherd",
"3": "Tan body, brown eared, long furred terrier"
},
"Skin": {
"1": "Pale, beige undertone",
"2": "Tan, pink undertone",
"3": "Pale, pink undertone",
"4": "Pale",
"5": "Brown, red undertone",
"6": "Brown, pink undertone",
"7": "Brown, warm tone",
"8": "Beige, orange undertone",
"9": "Light Brown",
"10": "Pale, pink overtone",
"11": "Pale, grey undertone",
"12": "Tan, warm tone",
"13": "Pale Green",
"14": "Pale Fuchsia",
"15": "Brown, full tone",
"16": "Tan, red overtone",
"17": "Pale Blue",
"18": "Green",
"19": "Pale Red",
"20": "Pale Purple",
"21": "Yellow, full tone",
"22": "Gray",
"23": "Pale, yellow undertone",
"24": "Pale, ivory undertone"
},
"Hair": {
"1": "Right-side part, short and unkempt",
"2": "Middle-part, six inches length",
"3": "Left-side part, swept bangs, combed back",
"4": "Afro",
"5": "Right-side part, unkempt with bangs",
"6": "Shaved back and sides",
"7": "Right-part open pompadour, chin length",
"8": "Right-part, short and combed back",
"9": "Right-part with bangs, large wayward spikes",
"10": "Right-part, side bob",
"11": "Pompadour, short and combed back",
"12": "Short with faded sides, combed back",
"13": "Middle-part, low ponytail",
"14": "Wayward dreads, six inches length, undershave",
"15": "Left-part with long bang, combed back",
"16": "Middle-part, 4 inches length, undercut",
"17": "Right-part, high ponytail, swept bangs",
"18": "Right-side part, shoulder length, low pigtails",
"19": "Right-side part, short with long swept bangs",
"20": "Updo, three tight buns on top of head",
"21": "Short and combed",
"22": "Right-side part, short, high pigtails",
"23": "Right-side part with bangs, tight high bun",
"24": "Right-side part with bangs, unkempt, six inches",
"25": "Right-side part, swept bangs, mid-back length",
"26": "Fifties style, teased, curly ended bob",
"27": "Middle-part, thigh-length",
"28": "Right-side part, swept bangs, chin length",
"29": "Middle-part, waist length, low ponytail",
"30": "Waist length with bangs, straight, tapered ends",
"31": "Right-side part with bangs, low pigtails",
"32": "Dual twisted side-buns, Princess Leia style",
"33": "Right-side part, swept bangs, short",
"34": "Right-side part, hip-length, pigtail braids",
"35": "Right-side part, mid-back length, pigtail braids",
"36": "High ponytail, mini bangs",
"37": "Middle-part, swept over right shoulder",
"38": "Right side part with bangs, high pigtails",
"39": "Black hairband, chin length",
"40": "Black hairband with bangs, shoulder length",
"41": "Left-side part, loose curls, shoulder length",
"42": "Shoulder length with mini bangs, curly",
"43": "Long on top with highlights, combed back",
"44": "Right-side part, swept bangs, short",
"45": "Middle-part, fade with 4 inches on top",
"46": "Cornrows, chin length",
"47": "Left-side part, short and combed",
"48": "Middle-part, swept bangs, chin length",
"49": "Middle-part, unkempt, partial ponytail",
"50": "Liberty spike style, shaved sides",
"51": "Donut cut, shoulder length",
"52": "Donut cut, short",
"53": "Bald or shaved",
"54": "Shaved, half-inch length, widow's peak",
"55": "Shaved, half-inch length, unkempt",
"56": "Shaved, half-inch length, straight hairline",
"101": "Left-side part with bangs, wavy, waist length",
"102": "Right-side part, hip-length, curly",
"103": "Right-side part, waist length, straight",
"104": "Middle-part, waist length, low ponytail",
"105": "Middle-part, waist length, high braid",
"106": "Right-side part with bangs, swept to shoulder",
"107": "Right-side part, unkempt, swept to shoulder",
"108": "Bob with bangs",
"109": "Left-side part, short, combed",
"110": "Wavy with bangs, 8 inches",
"111": "Wavy with bangs, shoulder length",
"112": "Dreads, neat, 4 inches length",
"113": "Short and unkempt",
"114": "Middle-part, six inches length",
"115": "Right-side part, shoulder length, unkempt",
"116": "Middle-part, teased, shoulder length",
"117": "Middle-part with bangs, short",
"118": "Left-side part with bangs, unkempt, short"
},
"Shirt": {
"1": "Red, denim overalls",
"2": "Brown button up",
"3": "Light Green, brown belt",
"4": "Black, gray splat design",
"5": "Black skull design",
"6": "Blue Gray, cloud design",
"7": "Cream, light blue horizontal stripe",
"8": "Green, denim overalls",
"9": "Yellow, brown horizontal zig zag",
"10": "Blue Green, cloud design",
"11": "Black, white letter A",
"12": "Green, collar cinches",
"13": "Lime Green, green stripes",
"14": "Red, white horizontal stripes",
"15": "Black, white ribcage design",
"16": "Brown, Tan, Light Brown stripes",
"17": "Blue, yellow dots",
"18": "Green, brown suspenders",
"19": "Brown jacket, Gray tee",
"20": "White, blue kerchief",
"21": "Green tank, Gray tee",
"22": "Ochre, green horizontal stripe",
"23": "Red button up",
"24": "Green button up",
"25": "Light Blue button up",
"26": "Blue button up",
"27": "Sea Green, horizontal white stripe",
"28": "Purple, light equal sign design",
"29": "Black, purple heart design",
"30": "White vertical gradient",
"31": "Brown jacket, Black shirt",
"32": "Brown Gray, angled button up",
"33": "Red, brown belt",
"34": "Green, strung collar",
"35": "Green bodice, gold belt, brown sleeves",
"36": "Red, white collar, buttoned",
"37": "Light Purple, zippered",
"38": "Gray to Black vertical gradient",
"39": "White, wide collar",
"40": "Sea Green and Brown stripes",
"41": "Purple vertical gradient",
"42": "White, horizontal cream stripe",
"43": "Green vertical gradient, belt",
"44": "Blue vertical gradient",
"45": "Blue, strung collar, white spot",
"46": "Brown vertical gradient",
"47": "Purple Vertical Gradient",
"48": "Brown, silver belt",
"49": "Black, gray bat design",
"50": "Light Purple, purple stripe",
"51": "Light Pink tank, purple shirt",
"52": "Pink tank, light purple tee",
"53": "Purple, vertical rainbow column",
"54": "Black, green belt",
"55": "Sea Green, white shoulder stripe",
"56": "Red, horizontal yellow stripe",
"57": "Lime Green, wide collar",
"58": "White and Gray stripes, red vest",
"59": "Blue, light blue shoulder stripe",
"60": "Ochre, yellow shoulder stripe",
"61": "Blue, wide collar",
"62": "Tan, stripes and dots",
"63": "Blue, white collar and stripe",
"64": "Red, silver collar",
"65": "Patchwork Blue",
"66": "Green, white undershirt",
"67": "Gray, mouse face design",
"68": "Yellow, low overalls",
"69": "Light Green, upper frog face",
"70": "Green, brown belt",
"71": "Fuchsia, light purple stripe",
"72": "White, denim overalls, brown belt",
"73": "Cream crop hoodie, blue tank",
"74": "Dark Blue and Purple horizontal split",
"75": "Blue, red overalls, brown belt",
"76": "Black, green mushroom cloud design",
"77": "Light Purple, brown belt",
"78": "White, tongue out frowny face",
"79": "Purple, white kerchief",
"80": "Black, blue overalls",
"81": "Gray, white shoulder stripe",
"82": "Green, light green waist stripe",
"83": "Dark Blue",
"84": "Black, wide collar",
"85": "Black",
"86": "Red, button up, open neck",
"87": "Teal, brown suspenders",
"88": "White button up, red kerchief",
"89": "Yellow, green vest",
"90": "Purple Bowling Style",
"91": "Black Hoodie",
"92": "Green, collared, white kerchief",
"93": "Pink, light pink shoulder stripe",
"94": "White, black spots",
"95": "Brown, red and yellow tie",
"96": "Yellow, black eyes with blush",
"97": "Green, dark green spots",
"98": "Gray, button up, dark vertical stripe",
"99": "Black peacoat, white shirt collar",
"100": "Purple, black overalls",
"101": "Light Blue, horizontal dark stripe",
"102": "Black, white front",
"103": "Canvas, blond leather belt",
"104": "Gray stripes, black overalls",
"105": "Green and Teal stripes",
"106": "Blue, white letter J",
"107": "Green and Black Horizontal split",
"108": "Fuchsia, white shoulder stripe",
"109": "Brown Orange",
"110": "Purple button up, dark vertical stripe",
"111": "Brown button up, dark vertical stripe",
"112": "Olive green, dark vertical stripe"
},
"Pants Style": {
"1": "Long",
"2": "Shorts",
"3": "Long Skirt",
"4": "Skirt"
},
"Accessory": {
"1": "Blank",
"2": "Full beard and mustache",
"3": "Full mustache",
"4": "Full mustache wrinkles",
"5": "Goatee",
"6": "Mutton chops",
"7": "Full beard and mustache, untrimmed",
"8": "Gold earrings",
"9": "Turquoise earrings",
"10": "Black full-frame glasses",
"11": "Lipstick",
"12": "Top-frame glasses",
"13": "Bushy eyebrows",
"14": "Robo-visor",
"15": "Circular black frame glasses",
"16": "Red necklace",
"17": "Black sunglasses",
"18": "Blue necklace",
"19": "Gray sunglasses",
"20": "Orange beak"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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