diff --git a/.github/workflows/mirrorchyan_uploading.yml b/.github/workflows/mirrorchyan_uploading.yml index 2add19eb..e650ef7b 100644 --- a/.github/workflows/mirrorchyan_uploading.yml +++ b/.github/workflows/mirrorchyan_uploading.yml @@ -6,8 +6,8 @@ on: types: [released] jobs: - mirrorchyan_7z: - runs-on: macos-latest + mirrorchyan: + runs-on: windows-latest steps: - name: 📥 Download release uses: robinraju/release-downloader@v1.8 @@ -15,14 +15,48 @@ jobs: latest: true fileName: "*" - - name: Extract 7z + - name: 📥 Download kachina-builder release + uses: robinraju/release-downloader@v1.8 + with: + repository: "YuehaiTeam/kachina-installer" + latest: true + fileName: "kachina-builder.exe" + + - name: Embed Dotnet and VCRedist shell: bash run: | + # dotnet + VERSION_URL="https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/8.0/latest.version" + VERSION=$(curl -fsSL "$VERSION_URL" | tr -d '\r\n') + if [ -z "$VERSION" ]; then + echo "Cannot get the latest version from $VERSION_URL" + exit 1 + fi + INSTALLER_URL="https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/${VERSION}/windowsdesktop-runtime-${VERSION}-win-x64.exe" + OUTPUT="windowsdesktop-runtime-${VERSION}-win-x64.exe" + echo "Downloading Windows Desktop Runtime version $VERSION from $INSTALLER_URL" + curl -fSL -o "$OUTPUT" "$INSTALLER_URL" + # vcredist + echo "Downloading VCRedist" + curl -fSL -o "vc_redist.x64.exe" "https://aka.ms/vs/17/release/vc_redist.x64.exe" + # embed + echo "Embedding runtimes" + ./kachina-builder.exe append -o ./BetterGI.Install.*.exe -f "$OUTPUT" -n "Microsoft.DotNet.DesktopRuntime.8" -f "vc_redist.x64.exe" -n "Microsoft.VCRedist.2015+.x64" + ./kachina-builder.exe extract -i ./BetterGI.Install.*.exe -f ./metadata.json -n "\0META" + + - name: Extract 7z and create zip + shell: bash + run: | + 7z a -tzip BetterGI.Install.zip ./BetterGI.Install.*.exe -mx=1 -r -y + choco install wget --no-progress 7z x BetterGI_v*.7z -oun7z - mv ./BetterGI.Metadata.json ./un7z/BetterGI/.metadata.json + mv ./metadata.json ./un7z/BetterGI/.metadata.json + cd un7z + 7z a -tzip ../BetterGI.zip ./BetterGI -mx=5 -r -y - name: Determine version number id: get-version + shell: bash run: | if [ "${{ github.event_name }}" = "release" ]; then echo "version=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT @@ -38,25 +72,17 @@ jobs: with: filetype: local mirrorchyan_rid: BGI - working-directory: ./un7z/ - pick_files: '["BetterGI"]' + filename: "BetterGI.zip" version_name: ${{ steps.get-version.outputs.version }} upload_token: ${{ secrets.MirrorChyanUploadToken }} - - mirrorchyan_exe: - runs-on: macos-latest - steps: - - uses: MirrorChyan/uploading-action@v1 - with: - filetype: latest-release - filename: "BetterGI.Install.*.exe" - extra_zip: true - mirrorchyan_rid: BGI - - github_token: ${{ secrets.GITHUB_TOKEN }} - owner: babalae - repo: better-genshin-impact - upload_token: ${{ secrets.MirrorChyanUploadToken }} os: win arch: x64 + - name: Upload Exe + uses: MirrorChyan/uploading-action@v1 + with: + filetype: local + mirrorchyan_rid: BGI + filename: "BetterGI.Install.zip" + version_name: ${{ steps.get-version.outputs.version }} + upload_token: ${{ secrets.MirrorChyanUploadToken }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index db2a785a..6dd9c99b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,5 @@ name: BetterGI Publish +run-name: "BetterGI ${{ inputs.version }}" on: workflow_dispatch: @@ -299,8 +300,8 @@ jobs: path: dist/BetterGI.Install.*.exe - uses: actions/upload-artifact@v4 with: - name: BetterGI_Metadata - path: dist/metadata.json + name: BetterGI_OnlineInst + path: dist/BetterGI/BetterGI.update.exe build_setup: runs-on: windows-latest @@ -349,11 +350,6 @@ jobs: - uses: actions/download-artifact@v4 with: path: artifacts - - - name: Append Metadata - shell: bash - run: | - mv artifacts/BetterGI_Metadata/metadata.json artifacts/BetterGI_Metadata/BetterGI_Metadata.json - name: Create Release uses: softprops/action-gh-release@v1 @@ -366,45 +362,86 @@ jobs: files: | artifacts/BetterGI_7z/*.7z artifacts/BetterGI_Install/*.exe - artifacts/BetterGI_Metadata/*.json mirrorchyan_uploading: if: github.repository_owner == 'babalae' && contains(needs.validate.outputs.version, '-') needs: [validate, build_dist, build_installer] - runs-on: macos-latest + runs-on: windows-latest steps: - uses: actions/download-artifact@v4 with: path: downloads github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: 📥 Download kachina-builder release + if: ${{ github.event.inputs.kachina-channel == 'release' }} + uses: robinraju/release-downloader@v1.8 + with: + repository: "YuehaiTeam/kachina-installer" + latest: true + fileName: "kachina-builder.exe" + - name: 📥 Download kachina-builder dev + if: ${{ github.event.inputs.kachina-channel == 'dev' }} + uses: dawidd6/action-download-artifact@v8 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + repo: "YuehaiTeam/kachina-installer" + workflow: "build.yml" + name: artifact + branch: main + event: push + workflow_conclusion: success - - name: Extract 7z + - name: Embed Dotnet and VCRedist shell: bash run: | + # dotnet + VERSION_URL="https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/8.0/latest.version" + VERSION=$(curl -fsSL "$VERSION_URL" | tr -d '\r\n') + if [ -z "$VERSION" ]; then + echo "Cannot get the latest version from $VERSION_URL" + exit 1 + fi + INSTALLER_URL="https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/${VERSION}/windowsdesktop-runtime-${VERSION}-win-x64.exe" + OUTPUT="windowsdesktop-runtime-${VERSION}-win-x64.exe" + echo "Downloading Windows Desktop Runtime version $VERSION from $INSTALLER_URL" + curl -fSL -o "$OUTPUT" "$INSTALLER_URL" + # vcredist + echo "Downloading VCRedist" + curl -fSL -o "vc_redist.x64.exe" "https://aka.ms/vs/17/release/vc_redist.x64.exe" + # embed + echo "Embedding runtimes" + ./kachina-builder.exe append -o ./downloads/BetterGI_Install/BetterGI.Install.*.exe -f "$OUTPUT" -n "Microsoft.DotNet.DesktopRuntime.8" -f "vc_redist.x64.exe" -n "Microsoft.VCRedist.2015+.x64" + ./kachina-builder.exe extract -i ./downloads/BetterGI_Install/BetterGI.Install.*.exe -f ./metadata.json -n "\0META" + - name: Extract 7z and create zip + shell: bash + run: | + choco install wget --no-progress + 7z a -tzip BetterGI.Install.${{ needs.validate.outputs.version }}.zip ./downloads/BetterGI_Install/BetterGI.Install.${{ needs.validate.outputs.version }}.exe -mx=1 -r -y cd downloads/BetterGI_7z 7z x BetterGI_v*.7z -oun7z - mv ../BetterGI_Metadata/metadata.json ./un7z/BetterGI/.metadata.json + mv ../../metadata.json ./un7z/BetterGI/.metadata.json + cd un7z + 7z a -tzip ../BetterGI.zip ./BetterGI -mx=5 -r -y - name: Upload Zip uses: MirrorChyan/uploading-action@v1 with: filetype: local mirrorchyan_rid: BGI - working-directory: downloads/BetterGI_7z/un7z/ - pick_files: '["BetterGI"]' + filename: "downloads/BetterGI_7z/BetterGI.zip" version_name: ${{ needs.validate.outputs.version }} upload_token: ${{ secrets.MirrorChyanUploadToken }} - + os: win + arch: x64 + - name: Upload Install.exe uses: MirrorChyan/uploading-action@v1 with: filetype: local mirrorchyan_rid: BGI - working-directory: downloads/BetterGI_Install/ - filename: "BetterGI.Install.*.exe" - extra_zip: true + filename: "BetterGI.Install.${{ needs.validate.outputs.version }}.zip" version_name: ${{ needs.validate.outputs.version }} upload_token: ${{ secrets.MirrorChyanUploadToken }} - os: win - arch: x64 + diff --git a/BetterGenshinImpact/App.xaml b/BetterGenshinImpact/App.xaml index 3aedc8a1..78242db4 100644 --- a/BetterGenshinImpact/App.xaml +++ b/BetterGenshinImpact/App.xaml @@ -11,7 +11,9 @@ + + /Assets/Fonts/MiSans-Regular.ttf#MiSans /Assets/Fonts/deluge-led.ttf#Deluge LED diff --git a/BetterGenshinImpact/Assets/Model/Common/avatar_side_classify_sim.onnx b/BetterGenshinImpact/Assets/Model/Common/avatar_side_classify_sim.onnx index fc825a5a..1ba4857b 100644 Binary files a/BetterGenshinImpact/Assets/Model/Common/avatar_side_classify_sim.onnx and b/BetterGenshinImpact/Assets/Model/Common/avatar_side_classify_sim.onnx differ diff --git a/BetterGenshinImpact/Assets/Model/LICENSE b/BetterGenshinImpact/Assets/Model/LICENSE new file mode 100644 index 00000000..9f520f9f --- /dev/null +++ b/BetterGenshinImpact/Assets/Model/LICENSE @@ -0,0 +1,693 @@ +======================================================================== +Section 1 (Overview): + +All models trained by the BetterGI organization within the current +directory are distributed under the GNU General Public License +version 3 (GPLv3). This license ensures that the models remain +free and open for use, modification, and distribution by anyone, +while also protecting the rights of the original creators and +maintaining the open-source nature of the software ecosystem. + +If you use any of the BetterGI models in your projects, regardless +of whether the usage is direct or integrated as part of a larger +system, you are required to open - source your entire codebase. +This “full - chain” requirement means that every piece of code that +interacts with or is influenced by the BetterGI models must be made +publicly available under the GPLv3 license. + +======================================================================== +Section 2 (Open Source License): + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + BetterGI Copyright (C) 2025 huiyadanli + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/BetterGenshinImpact/BetterGenshinImpact.csproj b/BetterGenshinImpact/BetterGenshinImpact.csproj index ce9ed196..3eb9d7d6 100644 --- a/BetterGenshinImpact/BetterGenshinImpact.csproj +++ b/BetterGenshinImpact/BetterGenshinImpact.csproj @@ -2,7 +2,7 @@ BetterGI - 0.45.3-alpha.1 + 0.46.3-alpha.1 false WinExe net8.0-windows10.0.22621.0 @@ -43,11 +43,13 @@ + + diff --git a/BetterGenshinImpact/Core/Recognition/ONNX/SVTR/TextInferenceFactory.cs b/BetterGenshinImpact/Core/Recognition/ONNX/SVTR/TextInferenceFactory.cs index a83620a2..d4606e45 100644 --- a/BetterGenshinImpact/Core/Recognition/ONNX/SVTR/TextInferenceFactory.cs +++ b/BetterGenshinImpact/Core/Recognition/ONNX/SVTR/TextInferenceFactory.cs @@ -16,32 +16,32 @@ public class TextInferenceFactory _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; } - public static Mat PreProcessForInference(Mat mat) - { - if (mat.Channels() == 3) - { - mat = mat.CvtColor(ColorConversionCodes.BGR2GRAY); - } - else if (mat.Channels() == 4) - { - mat = mat.CvtColor(ColorConversionCodes.BGRA2GRAY); - } - else if (mat.Channels() != 1) - { - throw new ArgumentException("mat must be 1, 3 or 4 channels"); - } - - // Yap 已经改用灰度图了 https://github.com/Alex-Beng/Yap/commit/c2ad1e7b1442aaf2d80782a032e00876cd1c6c84 - // 二值化 - // Cv2.Threshold(mat, mat, 0, 255, ThresholdTypes.Otsu | ThresholdTypes.Binary); - //Cv2.AdaptiveThreshold(mat, mat, 255, AdaptiveThresholdTypes.GaussianC, ThresholdTypes.Binary, 31, 3); // 效果不错 但是和模型不搭 - //mat = OpenCvCommonHelper.Threshold(mat, Scalar.FromRgb(235, 235, 235), Scalar.FromRgb(255, 255, 255)); // 识别物品不太行 - // 不知道为什么要强制拉伸到 221x32 - mat = ResizeHelper.ResizeTo(mat, 221, 32); - // 填充到 384x32 - var padded = new Mat(new Size(384, 32), MatType.CV_8UC1, Scalar.Black); - padded[new Rect(0, 0, mat.Width, mat.Height)] = mat; - //Cv2.ImWrite(Global.Absolute("padded.png"), padded); - return padded; - } + // public static Mat PreProcessForInference(Mat mat) + // { + // if (mat.Channels() == 3) + // { + // mat = mat.CvtColor(ColorConversionCodes.BGR2GRAY); + // } + // else if (mat.Channels() == 4) + // { + // mat = mat.CvtColor(ColorConversionCodes.BGRA2GRAY); + // } + // else if (mat.Channels() != 1) + // { + // throw new ArgumentException("mat must be 1, 3 or 4 channels"); + // } + // + // // Yap 已经改用灰度图了 https://github.com/Alex-Beng/Yap/commit/c2ad1e7b1442aaf2d80782a032e00876cd1c6c84 + // // 二值化 + // // Cv2.Threshold(mat, mat, 0, 255, ThresholdTypes.Otsu | ThresholdTypes.Binary); + // //Cv2.AdaptiveThreshold(mat, mat, 255, AdaptiveThresholdTypes.GaussianC, ThresholdTypes.Binary, 31, 3); // 效果不错 但是和模型不搭 + // //mat = OpenCvCommonHelper.Threshold(mat, Scalar.FromRgb(235, 235, 235), Scalar.FromRgb(255, 255, 255)); // 识别物品不太行 + // // 不知道为什么要强制拉伸到 221x32 + // mat = ResizeHelper.ResizeTo(mat, 221, 32); + // // 填充到 384x32 + // var padded = new Mat(new Size(384, 32), MatType.CV_8UC1, Scalar.Black); + // padded[new Rect(0, 0, mat.Width, mat.Height)] = mat; + // //Cv2.ImWrite(Global.Absolute("padded.png"), padded); + // return padded; + // } } diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/FastSqDiffMatcher.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/FastSqDiffMatcher.cs new file mode 100644 index 00000000..4fd36bf9 --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/FastSqDiffMatcher.cs @@ -0,0 +1,133 @@ +using OpenCvSharp; +using System; +using System.Linq; + +namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +public class FastSqDiffMatcher : IDisposable +{ + public readonly Mat[] Source; + private readonly Mat[] _sourceSq; + + /// + /// 初始化模板匹配器 + /// + /// 源图像 + /// 模板尺寸 + public FastSqDiffMatcher(Mat source, Size templateSize) + { + if (source.Empty()) + throw new Exception("源图像为空"); + if (templateSize.Width > source.Width || templateSize.Height > source.Height) + throw new Exception("模板图尺寸超过源图片尺寸"); + + Source = source.Split(); + + using Mat sourceF = new Mat(); + source.ConvertTo(sourceF, MatType.CV_32F); + Cv2.Multiply(sourceF, sourceF, sourceF); + _sourceSq = sourceF.Split(); + } + + /// + /// 执行模板匹配 + /// + /// 模板图像 + /// 遮罩图像 + /// 最佳匹配位置 (Point, double) + public (Point Loc, double Val) Match(Mat[] maskedTemplates, Mat maskF) + { + return Match(Source, _sourceSq, maskedTemplates, maskF); + } + + public (Point Loc, double Val) Match(Mat[] maskedTemplates, Mat maskF, Rect rect, int[]? channels = null) + { + var sourceRoi = SelectMatsByIndex(GetRegionViews(Source, rect), channels); + var sourceSqRoi = SelectMatsByIndex(GetRegionViews(_sourceSq, rect), channels); + var maskedTemplatesSelect = SelectMatsByIndex(maskedTemplates, channels); + return Match(sourceRoi, sourceSqRoi, maskedTemplatesSelect, maskF); + } + + private (Point Loc, double Val) Match(Mat[] source, Mat[] sourceSq, Mat[] maskedTemplates, Mat maskF) + { + var n = source.Length; + if (maskedTemplates.Length != n) + throw new Exception($"模板图通道数 {maskedTemplates.Length} 与源图像通道数 {n} 不匹配"); + + // 计算互相关图 + using var crossCorr = new Mat(); + using var temp = new Mat(); + Cv2.MatchTemplate(source[0], maskedTemplates[0], crossCorr, TemplateMatchModes.CCorr); + for (var i = 1; i < n; i++) + { + Cv2.MatchTemplate(source[i], maskedTemplates[i], temp, TemplateMatchModes.CCorr); + Cv2.Add(crossCorr, temp, crossCorr); + } + + Cv2.Multiply(crossCorr, -2, crossCorr); + // 计算源图像与遮罩的加权平方图 + for (var i = 0; i < n; i++) + { + Cv2.MatchTemplate(sourceSq[i], maskF, temp, TemplateMatchModes.CCorr); + Cv2.Add(crossCorr, temp, crossCorr); + } + Cv2.MinMaxLoc(crossCorr, out var minVal, out _, out var minLoc, out _); + return (minLoc, minVal); + } + + /// + /// + /// + /// 模板图 + /// 遮罩, 类型为 8UC1, 且尺寸与 template 相同 + /// + /// + public static (Mat[] maskedTemplates, Mat maskF) PreProcess(Mat template, Mat mask) + { + if (mask.Type() != MatType.CV_8UC1) + throw new Exception("遮罩格式不对"); + if (template.Size() != mask.Size()) + throw new Exception("模板图与遮罩尺寸不匹配"); + + var maskF = new Mat(); + mask.ConvertTo(maskF, MatType.CV_32F); + Cv2.Normalize(maskF, maskF, 0, 1, NormTypes.MinMax); + + using var maskedTemplate = new Mat(template.Size(), template.Type(), Scalar.All(0)); + Cv2.BitwiseAnd(template, template, maskedTemplate, mask); + + var maskedTemplates = maskedTemplate.Split(); + + return (maskedTemplates, maskF); + } + + public static double GetTplSumSq(Mat[] maskedTemplates, int[]? channels = null) + { + return SelectMatsByIndex(maskedTemplates, channels).Sum(maskedTpl => Cv2.Norm(maskedTpl, NormTypes.L2SQR)); + } + + public static Mat[] SelectMatsByIndex(Mat[] matArray, int[]? channels = null) + { + return channels?.Where(index => index >= 0 && index < matArray.Length) + .Select(index => matArray[index]) + .ToArray() + ?? matArray; + } + + public static Mat[] GetRegionViews(Mat[] images, Rect rect) + { + return images.Select(img => img.SubMat(rect)).ToArray(); + } + + public void Dispose() + { + foreach (var img in Source) + { + img.Dispose(); + } + foreach (var img in _sourceSq) + { + img.Dispose(); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/MatchContext.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/MatchContext.cs new file mode 100644 index 00000000..419ce534 --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/MatchContext.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using BetterGenshinImpact.Core.Config; +using OpenCvSharp; +using static BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch.MiniMapMatchConfig; + +namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +public class MatchContext : IDisposable +{ + public Mat[] MaskedMiniMapRoughs; + public Mat MaskRoughF; + public Mat MiniMapExact = new Mat(); + public Mat MaskExact = new Mat(); + public TemplateMatchNormalizer NormalizerRough; + public TemplateMatchNormalizer NormalizerRoughChan; + public TemplateMatchNormalizer NormalizerExact; + public double TplSumSq; + public double TplSumSqChan; + public int[] Channels = [0, 1]; + private bool _disposed = false; + + public MatchContext(Mat miniMap, Mat mask) + { + /* + var name1 = "miniMap.png"; + var path = Global.Absolute($@"log\screenshot\{name1}"); + Cv2.ImWrite(path, miniMap); + var name2 = "mask.png"; + path = Global.Absolute($@"log\screenshot\{name2}"); + Cv2.ImWrite(path, mask); + */ + GetRoughMiniMap(miniMap, mask); + GetExactMiniMap(miniMap, mask); + } + + public void GetRoughMiniMap(Mat miniMap, Mat mask) + { + using var miniMapRough = new Mat(); + using var maskRough = new Mat(); + Cv2.Resize(miniMap, miniMapRough, new Size(RoughSize, RoughSize), interpolation: InterpolationFlags.Area); + Cv2.Resize(mask, maskRough, new Size(RoughSize, RoughSize), interpolation: InterpolationFlags.Nearest); + (MaskedMiniMapRoughs, MaskRoughF) = FastSqDiffMatcher.PreProcess(miniMapRough, maskRough); + TplSumSq = FastSqDiffMatcher.GetTplSumSq(MaskedMiniMapRoughs); + TplSumSqChan = FastSqDiffMatcher.GetTplSumSq(MaskedMiniMapRoughs, Channels); + NormalizerRough = new TemplateMatchNormalizer(MaskedMiniMapRoughs, maskRough); + NormalizerRoughChan = new TemplateMatchNormalizer(MaskedMiniMapRoughs, maskRough, TemplateMatchModes.SqDiff, Channels); + } + + public void GetExactMiniMap(Mat miniMap, Mat mask) + { + using var miniMapGray = new Mat(); + Cv2.CvtColor(miniMap, miniMapGray, ColorConversionCodes.BGR2GRAY); + Cv2.Resize(miniMapGray, MiniMapExact, new Size(ExactSize, ExactSize), interpolation: InterpolationFlags.Cubic); + Cv2.Resize(mask, MaskExact, new Size(ExactSize, ExactSize), interpolation: InterpolationFlags.Nearest); + NormalizerExact = new TemplateMatchNormalizer(MiniMapExact, MaskExact); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + + if (disposing) + { + // 释放托管资源 + DisposeMatArray(ref MaskedMiniMapRoughs); + MaskRoughF.Dispose(); + MiniMapExact.Dispose(); + MaskExact.Dispose(); + } + _disposed = true; + } + + private void DisposeMatArray(ref Mat[] matArray) + { + foreach (var mat in matArray) + { + mat.Dispose(); + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/MiniMapMatchConfig.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/MiniMapMatchConfig.cs new file mode 100644 index 00000000..e23477af --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/MiniMapMatchConfig.cs @@ -0,0 +1,29 @@ +namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +public static class MiniMapMatchConfig +{ + /// + /// 原始小地图尺寸 + /// + public const int OriginalSize = 156; + + /// + /// 粗匹配时的地图尺寸 + /// + public const int RoughSize = 52; + + /// + /// 精确匹配时的地图尺寸 + /// + public const int ExactSize = 260; + + public const int GlobalScale = 2; + + public const int RoughZoom = 5; + public const int ExactZoom = 1; + public const int RoughSearchRadius = 50; + public const int ExactSearchRadius = 20; + public static readonly float HighThreshold = 0.99f; + public static readonly float LowThreshold = 0.9f; + +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/SubPixMatch.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/SubPixMatch.cs new file mode 100644 index 00000000..0849763b --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/SubPixMatch.cs @@ -0,0 +1,57 @@ +using System; +using OpenCvSharp; + +namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +public static class SubPixMatch +{ + private static readonly Mat Features = Mat.FromPixelData(6, 9, MatType.CV_64F, new double[,] + { + {1/6.0, -(1/3.0), 1/6.0, 1/6.0, -(1/3.0), 1/6.0, 1/6.0, -(1/3.0), 1/6.0}, + {1/6.0, 1/6.0, 1/6.0, -(1/3.0), -(1/3.0), -(1/3.0), 1/6.0, 1/6.0, 1/6.0}, + {1/4.0, 0, -(1/4.0), 0, 0, 0, -(1/4.0), 0, 1/4.0}, + {-(1/6.0), 0, 1/6.0, -(1/6.0), 0, 1/6.0, -(1/6.0), 0, 1/6.0}, + {-(1/6.0), -(1/6.0), -(1/6.0), 0, 0, 0, 1/6.0, 1/6.0, 1/6.0}, + {-(1/9.0), 2/9.0, -(1/9.0), 2/9.0, 5/9.0, 2/9.0, -(1/9.0), 2/9.0, -(1/9.0)} + }); + + //读取最值点周围3x3区域进行二次拟合 + + public static Point2f Fit(Mat src, Point loc) + { + // 参数校验 + if (src.Empty()) + throw new Exception("输入矩阵为空"); + if (src.Width < 3 || src.Height < 3) + throw new Exception("输入矩阵过小"); + if (loc.X < 0 || loc.Y < 0 || loc.X >= src.Width || loc.Y >= src.Height) + throw new Exception("输入点位超出范围"); + + // 边界约束:确保3x3邻域有效 + var clampedX = Math.Clamp(loc.X, 1, src.Width - 2); + var clampedY = Math.Clamp(loc.Y, 1, src.Height - 2); + // 提取并预处理3x3邻域 + var neighborhoodMatrix = src[new Rect(clampedX - 1, clampedY - 1, 3, 3)] + .Clone() + .Reshape(0, 9); // 展平为9x1向量 + neighborhoodMatrix.ConvertTo(neighborhoodMatrix, MatType.CV_64FC1); + + // 计算拟合系数 + Mat coefficientMatrix = Features * neighborhoodMatrix; + coefficientMatrix.GetArray(out var coefficients); + + // 计算二次方程判别式 + var discriminant = coefficients[2] * coefficients[2] - 4 * coefficients[0] * coefficients[1]; + const double epsilon = 1e-20; + if (Math.Abs(discriminant) < epsilon) + { + return loc; + } + // 计算偏移量并约束范围 + var offsetX = (2 * coefficients[1] * coefficients[3] - coefficients[2] * coefficients[4]) / discriminant; + var offsetY = (2 * coefficients[0] * coefficients[4] - coefficients[2] * coefficients[3]) / discriminant; + offsetX = Math.Clamp(offsetX, -1.0, 1.0); + offsetY = Math.Clamp(offsetY, -1.0, 1.0); + return new Point2f((float)(offsetX + clampedX), (float)(offsetY + clampedY)); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/TemplateMatchHelper.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/TemplateMatchHelper.cs new file mode 100644 index 00000000..ba4ae59d --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/TemplateMatchHelper.cs @@ -0,0 +1,46 @@ +using OpenCvSharp; +namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +public static class TemplateMatchHelper +{ + public static (Point, double) MatchTemplate(Mat image, Mat template, TemplateMatchModes method, Mat? mask = null) + { + using var result = new Mat(); + Cv2.MatchTemplate(image,template, result, method, mask); + Point loc; + double val; + if (method is TemplateMatchModes.SqDiff or TemplateMatchModes.SqDiffNormed) + { + Cv2.MinMaxLoc(result, out val, out _, out loc, out _); + } + else + { + Cv2.MinMaxLoc(result, out _, out val, out _, out loc); + } + return (loc, val); + } + + public static (Point, double) MatchTemplateNormalized(Mat image, Mat template, TemplateMatchModes method, Mat? mask = null) + { + var normalizer = new TemplateMatchNormalizer(template, mask, method); + (var loc, normalizer.Value) = MatchTemplate(image, template, method, mask); + return (loc, normalizer.Confidence()); + } + + public static (Point2f, double) MatchTemplateSubPix(Mat image, Mat template, TemplateMatchModes method, Mat? mask) + { + using var result = new Mat(); + Cv2.MatchTemplate(image,template, result, method, mask); + Point loc; + double val; + if (method is TemplateMatchModes.SqDiff or TemplateMatchModes.SqDiffNormed) + { + Cv2.MinMaxLoc(result, out val, out _, out loc, out _); + } + else + { + Cv2.MinMaxLoc(result, out _, out val, out _, out loc); + } + return (SubPixMatch.Fit(result, loc), val); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/TemplateMatchNormalizer.cs b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/TemplateMatchNormalizer.cs new file mode 100644 index 00000000..1514a52c --- /dev/null +++ b/BetterGenshinImpact/Core/Recognition/OpenCv/TemplateMatch/TemplateMatchNormalizer.cs @@ -0,0 +1,145 @@ +using System; +using System.Linq; +using OpenCvSharp; + +namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +public class TemplateMatchNormalizer +{ + public TemplateMatchModes Mode; + public double Value; + public int Sign; + private readonly double _bestMatchValue; + private readonly double _worstMatchValue; + + /// + /// 构造模板匹配归一化器 + /// + /// 模板匹配类型 + /// 模板图 + /// 遮罩 + public TemplateMatchNormalizer(Mat template, Mat? mask = null, TemplateMatchModes mode = TemplateMatchModes.SqDiff) + { + Init(mode); + (_bestMatchValue, _worstMatchValue) = mode switch + { + TemplateMatchModes.SqDiff => + SqDiffMatchValue(template, mask), + TemplateMatchModes.SqDiffNormed => + (0, 1), + TemplateMatchModes.CCorr => + CCorrMatchValue(template, mask), + TemplateMatchModes.CCorrNormed => + (1, 0), + TemplateMatchModes.CCoeff => + CCoeffMatchValue(template, mask), + TemplateMatchModes.CCoeffNormed => + (1, -1), + _ => throw new ArgumentException($"未知的模板匹配模式: {mode}", nameof(mode)) + }; + } + + public TemplateMatchNormalizer(Mat[] templates, Mat? mask = null, TemplateMatchModes mode = TemplateMatchModes.SqDiff, int[]? channels = null) + { + Init(mode); + (_bestMatchValue, _worstMatchValue) = mode switch + { + TemplateMatchModes.SqDiff => + SumMatchValue(templates, mask, SqDiffMatchValue, channels), + TemplateMatchModes.SqDiffNormed => + (0, 1), + TemplateMatchModes.CCorr => + SumMatchValue(templates, mask, CCorrMatchValue, channels), + TemplateMatchModes.CCorrNormed => + (1, 0), + TemplateMatchModes.CCoeff => + SumMatchValue(templates, mask, CCoeffMatchValue, channels), + TemplateMatchModes.CCoeffNormed => + (1, -1), + _ => throw new ArgumentException($"未知的模板匹配模式: {mode}", nameof(mode)) + }; + } + + public TemplateMatchNormalizer(double bestMatchValue, double worstMatchValue, TemplateMatchModes mode = TemplateMatchModes.SqDiff) + { + Init(mode); + _bestMatchValue = bestMatchValue; + _worstMatchValue = worstMatchValue; + } + + private void Init(TemplateMatchModes mode) + { + Mode = mode; + Sign = mode switch + { + TemplateMatchModes.SqDiff or TemplateMatchModes.SqDiffNormed => -1, + _ => 1 + }; + Reset(); + } + + /// + /// 更新 value + /// + /// + /// + public bool Update(double value) + { + if (double.IsPositiveInfinity(value) || double.IsNegativeInfinity(value)) + return false; + if (value > Math.Max(_bestMatchValue, _worstMatchValue) || value < Math.Min(_bestMatchValue, _worstMatchValue)) + return false; + if (Sign * (Value - value) >= 0) return false; + Value = value; + return true; + } + + public void Reset() + { + Value = Sign > 0 ? double.NegativeInfinity : double.PositiveInfinity; + } + + public double Confidence() + { + return (_bestMatchValue == _worstMatchValue)? 0 : + (Value - _worstMatchValue) / (_bestMatchValue - _worstMatchValue); + } + + public static (double, double) SumMatchValue(Mat[] templates, Mat? mask, Func getValue, int[]? channels = null) + { + var outValue = (0.0, 0.0); + var selectedTemplates = channels?.Where(i => i >= 0 && i < templates.Length) + .Select(i => templates[i]) + ?? templates; + foreach (var template in selectedTemplates) + { + var matchValue = getValue(template, mask); + outValue.Item1 += matchValue.Item1; + outValue.Item2 += matchValue.Item2; + } + return outValue; + } + + public static (double, double) SqDiffMatchValue(Mat template, Mat? mask = null) + { + using var inverted = new Mat(); + Cv2.Subtract(255, template, inverted); + Cv2.Max(template, inverted, inverted); + var worstVal = Cv2.Norm(inverted, NormTypes.L2SQR, mask); + return (0, worstVal); + } + + public static (double, double) CCorrMatchValue(Mat template, Mat? mask = null) + { + var bestVal = Cv2.Norm(template, NormTypes.L2SQR, mask); + return (bestVal, 0); + } + + public static (double, double) CCoeffMatchValue(Mat template, Mat? mask = null) + { + using var result = new Mat(); + Cv2.MatchTemplate(template, template, result, TemplateMatchModes.CCoeff, mask); + var bestVal = result.At(0, 0); + return (bestVal, -bestVal); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs index 1add05b0..0c514daf 100644 --- a/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs +++ b/BetterGenshinImpact/Core/Script/ScriptRepoUpdater.cs @@ -17,6 +17,8 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Windows; +using BetterGenshinImpact.View.Windows; +using LibGit2Sharp; using Vanara.PInvoke; using Wpf.Ui.Violeta.Controls; @@ -34,17 +36,19 @@ public class ScriptRepoUpdater : Singleton // 仓储临时目录 用于下载与解压 public static readonly string ReposTempPath = Path.Combine(ReposPath, "Temp"); - // 中央仓库信息地址 - public static readonly List CenterRepoInfoUrls = - [ - "https://raw.githubusercontent.com/babalae/bettergi-scripts-list/refs/heads/main/repo.json", - "https://r2-script.bettergi.com/github_mirror/repo.json", - ]; + // // 中央仓库信息地址 + // public static readonly List CenterRepoInfoUrls = + // [ + // "https://raw.githubusercontent.com/babalae/bettergi-scripts-list/refs/heads/main/repo.json", + // "https://r2-script.bettergi.com/github_mirror/repo.json", + // ]; // 中央仓库解压后文件夹名 - public static readonly string CenterRepoUnzipName = "bettergi-scripts-list-main"; + public static readonly string CenterRepoUnzipName = "bettergi-scripts-list-git"; public static readonly string CenterRepoPath = Path.Combine(ReposPath, CenterRepoUnzipName); + + public static readonly string CenterRepoPathOld = Path.Combine(ReposPath, "bettergi-scripts-list-main"); public static readonly Dictionary PathMapper = new Dictionary { @@ -56,103 +60,212 @@ public class ScriptRepoUpdater : Singleton private WebpageWindow? _webWindow; - public void AutoUpdate() + // [Obsolete] + // public void AutoUpdate() + // { + // var scriptConfig = TaskContext.Instance().Config.ScriptConfig; + // + // if (!Directory.Exists(ReposPath)) + // { + // Directory.CreateDirectory(ReposPath); + // } + // + // // 判断更新周期是否到达 + // if (DateTime.Now - scriptConfig.LastUpdateScriptRepoTime >= + // TimeSpan.FromDays(scriptConfig.AutoUpdateScriptRepoPeriod)) + // { + // // 更新仓库 + // Task.Run(async () => + // { + // try + // { + // var (repoPath, updated) = await UpdateCenterRepo(); + // Debug.WriteLine($"脚本仓库更新完成,路径:{repoPath}"); + // scriptConfig.LastUpdateScriptRepoTime = DateTime.Now; + // if (updated) + // { + // scriptConfig.ScriptRepoHintDotVisible = true; + // } + // } + // catch (Exception e) + // { + // _logger.LogDebug(e, $"脚本仓库更新失败:{e.Message}"); + // } + // }); + // } + // } + + + public async Task<(string, bool)> UpdateCenterRepoByGit(string repoUrl) { - var scriptConfig = TaskContext.Instance().Config.ScriptConfig; - - if (!Directory.Exists(ReposPath)) + if (string.IsNullOrEmpty(repoUrl)) { - Directory.CreateDirectory(ReposPath); + throw new ArgumentException("仓库URL不能为空", nameof(repoUrl)); } - // 判断更新周期是否到达 - if (DateTime.Now - scriptConfig.LastUpdateScriptRepoTime >= TimeSpan.FromDays(scriptConfig.AutoUpdateScriptRepoPeriod)) - { - // 更新仓库 - Task.Run(async () => - { - try - { - var (repoPath, updated) = await UpdateCenterRepo(); - Debug.WriteLine($"脚本仓库更新完成,路径:{repoPath}"); - scriptConfig.LastUpdateScriptRepoTime = DateTime.Now; - if (updated) - { - scriptConfig.ScriptRepoHintDotVisible = true; - } - } - catch (Exception e) - { - _logger.LogDebug(e, $"脚本仓库更新失败:{e.Message}"); - } - }); - } - } - - public async Task<(string, bool)> UpdateCenterRepo() - { - // 测速并获取信息 - var (fastUrl, jsonString) = await ProxySpeedTester.GetFastestUrlAsync(CenterRepoInfoUrls); - if (string.IsNullOrEmpty(jsonString)) - { - throw new Exception("从互联网下载最新的仓库信息失败"); - } - - var (time, url, file) = ParseJson(jsonString); - + var repoPath = Path.Combine(ReposPath, "bettergi-scripts-list-git"); var updated = false; - // 检查仓库是否存在,不存在则下载 - var needDownload = false; - if (Directory.Exists(CenterRepoPath)) + await Task.Run(() => { - var p = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); - if (p is null) + try { - needDownload = true; + GlobalSettings.SetOwnerValidation(false); + if (!Directory.Exists(repoPath)) + { + // 如果仓库不存在,执行浅克隆操作 + _logger.LogInformation($"浅克隆仓库: {repoUrl} 到 {repoPath}"); + + // 使用浅克隆选项 + var options = new CloneOptions + { + Checkout = true, + IsBare = false, + RecurseSubmodules = false, // 不递归克隆子模块 + }; + options.FetchOptions.Depth = 1; // 浅克隆,只获取最新的提交 + // 克隆仓库 + Repository.Clone(repoUrl, repoPath, options); + updated = true; + } + else + { + // 仓库已经存在,执行拉取更新 + using var repo = new Repository(repoPath); + + // 检查远程URL是否需要更新 + var origin = repo.Network.Remotes["origin"]; + if (origin.Url != repoUrl) + { + // 远程URL已更改,需要更新 + _logger.LogInformation($"更新远程URL: 从 {origin.Url} 到 {repoUrl}"); + repo.Network.Remotes.Update("origin", r => r.Url = repoUrl); + } + + // 获取远程分支信息 + var remote = repo.Network.Remotes["origin"]; + var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification); + + // 使用浅拉取选项 + // var fetchOptions = new FetchOptions + // { + // Depth = 1 // 浅拉取,只获取最新的提交 + // }; + + Commands.Fetch(repo, remote.Name, refSpecs, null, "拉取最新更新"); + + // 获取当前分支 + var branch = repo.Branches["main"] ?? repo.Branches["master"]; + if (branch == null) + { + throw new Exception("未找到main或master分支"); + } + + // 如果是本地分支,需要设置上游分支 + if (!branch.IsRemote) + { + var trackingBranch = repo.Branches[$"origin/{branch.FriendlyName}"]; + if (trackingBranch != null && branch.TrackedBranch == null) + { + branch = repo.Branches.Update(branch, + b => b.TrackedBranch = trackingBranch.CanonicalName); + } + } + + // 检查是否有更新 + var currentCommitSha = repo.Head.Tip.Sha; + + // 合并或重置到最新 + if (branch.TrackedBranch != null) + { + var trackingBranch = branch.TrackedBranch; + var mergeResult = Commands.Pull( + repo, + new Signature("BetterGI", "auto@bettergi.com", DateTimeOffset.Now), + new PullOptions()); + + // 检查是否有更新 + updated = currentCommitSha != repo.Head.Tip.Sha; + } + } } - } - else - { - needDownload = true; - } + catch (Exception ex) + { + _logger.LogError(ex, "Git仓库更新失败"); + throw; + } + }); - if (needDownload) - { - await DownloadRepoAndUnzip(url); - updated = true; - } - - // 搜索本地的 repo.json - var localRepoJsonPath = Directory.GetFiles(CenterRepoPath, file, SearchOption.AllDirectories).FirstOrDefault(); - if (localRepoJsonPath is null) - { - throw new Exception("本地仓库缺少 repo.json"); - } - - var (time2, url2, file2) = ParseJson(await File.ReadAllTextAsync(localRepoJsonPath)); - - // 检查是否需要更新 - if (long.Parse(time) > long.Parse(time2)) - { - await DownloadRepoAndUnzip(url2); - updated = true; - } - - // 获取与 localRepoJsonPath 同名(无扩展名)的文件夹路径 - var folderName = Path.GetFileNameWithoutExtension(localRepoJsonPath); - var folderPath = Path.Combine(Path.GetDirectoryName(localRepoJsonPath)!, folderName); - if (!Directory.Exists(folderPath)) - { - throw new Exception("本地仓库文件夹不存在"); - } - - return (folderPath, updated); + return (repoPath, updated); } + + + // [Obsolete] + // public async Task<(string, bool)> UpdateCenterRepo() + // { + // // 测速并获取信息 + // var (fastUrl, jsonString) = await ProxySpeedTester.GetFastestUrlAsync(CenterRepoInfoUrls); + // if (string.IsNullOrEmpty(jsonString)) + // { + // throw new Exception("从互联网下载最新的仓库信息失败"); + // } + // + // var (time, url, file) = ParseJson(jsonString); + // + // var updated = false; + // + // // 检查仓库是否存在,不存在则下载 + // var needDownload = false; + // if (Directory.Exists(CenterRepoPath)) + // { + // var p = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); + // if (p is null) + // { + // needDownload = true; + // } + // } + // else + // { + // needDownload = true; + // } + // + // if (needDownload) + // { + // await DownloadRepoAndUnzip(url); + // updated = true; + // } + // + // // 搜索本地的 repo.json + // var localRepoJsonPath = Directory.GetFiles(CenterRepoPath, file, SearchOption.AllDirectories).FirstOrDefault(); + // if (localRepoJsonPath is null) + // { + // throw new Exception("本地仓库缺少 repo.json"); + // } + // + // var (time2, url2, file2) = ParseJson(await File.ReadAllTextAsync(localRepoJsonPath)); + // + // // 检查是否需要更新 + // if (long.Parse(time) > long.Parse(time2)) + // { + // await DownloadRepoAndUnzip(url2); + // updated = true; + // } + // + // // 获取与 localRepoJsonPath 同名(无扩展名)的文件夹路径 + // var folderName = Path.GetFileNameWithoutExtension(localRepoJsonPath); + // var folderPath = Path.Combine(Path.GetDirectoryName(localRepoJsonPath)!, folderName); + // if (!Directory.Exists(folderPath)) + // { + // throw new Exception("本地仓库文件夹不存在"); + // } + // + // return (folderPath, updated); + // } public string FindCenterRepoPath() { - var localRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories).FirstOrDefault(); + var localRepoJsonPath = Directory.GetFiles(CenterRepoPath, "repo.json", SearchOption.AllDirectories) + .FirstOrDefault(); if (localRepoJsonPath is null) { throw new Exception("本地仓库缺少 repo.json"); @@ -197,7 +310,9 @@ public class ScriptRepoUpdater : Singleton // 获取文件名 var contentDisposition = res.Content.Headers.ContentDisposition; - var fileName = contentDisposition is { FileName: not null } ? contentDisposition.FileName.Trim('"') : "temp.zip"; + var fileName = contentDisposition is { FileName: not null } + ? contentDisposition.FileName.Trim('"') + : "temp.zip"; // 创建临时目录 if (!Directory.Exists(ReposTempPath)) @@ -250,7 +365,8 @@ public class ScriptRepoUpdater : Singleton var uiMessageBox = new Wpf.Ui.Controls.MessageBox { Title = "脚本订阅", - Content = $"检测到{(formClipboard ? "剪切板上存在" : "")}脚本订阅链接,解析后需要导入的脚本为:{pathJson}。\n是否导入并覆盖此文件或者文件夹下的脚本?", + Content = + $"检测到{(formClipboard ? "剪切板上存在" : "")}脚本订阅链接,解析后需要导入的脚本为:{pathJson}。\n是否导入并覆盖此文件或者文件夹下的脚本?", CloseButtonText = "关闭", PrimaryButtonText = "确认导入", Owner = Application.Current.MainWindow, @@ -322,28 +438,13 @@ public class ScriptRepoUpdater : Singleton scriptConfig.SubscribedScriptPaths.AddRange(paths); Toast.Information("获取最新仓库信息中..."); - - // 更新仓库 + string repoPath; try { - (repoPath, _) = await Task.Run(UpdateCenterRepo); + repoPath = FindCenterRepoPath(); } - catch (Exception e) - { - Toast.Warning("获取最新仓库信息失败,尝试使用本地已有仓库信息"); - try - { - repoPath = FindCenterRepoPath(); - } - catch - { - await MessageBox.ErrorAsync("本地无仓库信息,请至少成功更新一次脚本仓库信息!"); - return; - } - } - - if (string.IsNullOrEmpty(repoPath)) + catch { await MessageBox.ErrorAsync("本地无仓库信息,请至少成功更新一次脚本仓库信息!"); return; @@ -474,10 +575,13 @@ public class ScriptRepoUpdater : Singleton } // 使用路径分隔符分割路径 - string[] parts = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + string[] parts = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, + StringSplitOptions.RemoveEmptyEntries); // 返回第一个文件夹和剩余路径 - return parts.Length > 0 ? (parts[0], string.Join(Path.DirectorySeparatorChar, parts.Skip(1))) : (string.Empty, string.Empty); + return parts.Length > 0 + ? (parts[0], string.Join(Path.DirectorySeparatorChar, parts.Skip(1))) + : (string.Empty, string.Empty); } public void OpenLocalRepoInWebView() @@ -493,7 +597,8 @@ public class ScriptRepoUpdater : Singleton _webWindow.Closed += (s, e) => _webWindow = null; _webWindow.Panel!.DownloadFolderPath = MapPathingViewModel.PathJsonPath; _webWindow.NavigateToFile(Global.Absolute(@"Assets\Web\ScriptRepo\index.html")); - _webWindow.Panel!.OnWebViewInitializedAction = () => _webWindow.Panel!.WebView.CoreWebView2.AddHostObjectToScript("repoWebBridge", new RepoWebBridge()); + _webWindow.Panel!.OnWebViewInitializedAction = () => + _webWindow.Panel!.WebView.CoreWebView2.AddHostObjectToScript("repoWebBridge", new RepoWebBridge()); _webWindow.Show(); } else @@ -502,6 +607,12 @@ public class ScriptRepoUpdater : Singleton } } + public void OpenScriptRepoWindow() + { + var scriptRepoWindow = new ScriptRepoWindow { Owner = Application.Current.MainWindow }; + scriptRepoWindow.ShowDialog(); + } + /// /// 处理带有 icon.ico 和 desktop.ini 的文件夹 /// diff --git a/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs b/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs index 1c211e6c..2023a7ae 100644 --- a/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs +++ b/BetterGenshinImpact/Core/Script/WebView/RepoWebBridge.cs @@ -20,15 +20,6 @@ public class RepoWebBridge { public async Task GetRepoJson() { - try - { - await ScriptRepoUpdater.Instance.UpdateCenterRepo(); - } - catch (Exception e) - { - Toast.Warning($"更新仓库信息失败:{e.Message}"); - } - try { if (!Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) diff --git a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs index 23b3d5e2..68dd79d7 100644 --- a/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs +++ b/BetterGenshinImpact/GameTask/AutoArtifactSalvage/AutoArtifactSalvageTask.cs @@ -24,6 +24,7 @@ using BetterGenshinImpact.Core.Recognition.OpenCv; using BetterGenshinImpact.Core.Recognition.OCR; using BetterGenshinImpact.GameTask.Common; using BetterGenshinImpact.GameTask.Common.Job; +using GameTask.Model.GameUI; namespace BetterGenshinImpact.GameTask.AutoArtifactSalvage; @@ -210,118 +211,42 @@ public class AutoArtifactSalvageTask : ISoloTask private async Task Salvage5Star(string regularExpression, int maxNumToCheck) { int count = maxNumToCheck; - Queue checkedArtifactAffixesQueue = new Queue(); - int duplicateSum = 0; - while (count > 0 && duplicateSum < 3) + + using var ra0 = CaptureToRectArea(); + Rect gridRoi = new Rect((int)(ra0.Width * 0.025), (int)(ra0.Width * 0.055), (int)(ra0.Width * 0.66), (int)(ra0.Width * 0.4)); + GridScreen gridScreen = new GridScreen(gridRoi, 3, 40, 28, 0.018, this.logger, this.ct); // 圣遗物分解Grid有4行9列 + await foreach (ImageRegion itemRegion in gridScreen) { - // VisionContext.Instance().DrawContent.ClearAll(); - // await Delay(400, this.ct); - - using var ra = CaptureToRectArea(); - using ImageRegion grid = ra.DeriveCrop(new Rect((int)(ra.Width * 0.025), (int)(ra.Width * 0.055), (int)(ra.Width * 0.66), (int)(ra.Width * 0.4))); - IEnumerable gridItems = GetArtifactGridItems(grid.SrcMat); - - //foreach (Rect item in gridItems) - //{ - // grid.DrawRect(item, item.GetHashCode().ToString(), new System.Drawing.Pen(System.Drawing.Color.Blue)); - //} - - bool anyItemChecked = false; - foreach (Rect item in gridItems) + Rect gridRect = itemRegion.ToRect(); + if (GetArtifactStatus(itemRegion.SrcMat) == ArtifactStatus.None) { - using ImageRegion itemRegion = grid.DeriveCrop(item); - if (GetArtifactStatus(itemRegion.SrcMat) == ArtifactStatus.None) + itemRegion.Click(); + await Delay(300, ct); + + using var ra1 = CaptureToRectArea(); + using ImageRegion itemRegion1 = ra1.DeriveCrop(gridRect + new Point(gridRoi.X, gridRoi.Y)); + if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected) { - anyItemChecked = true; - itemRegion.Click(); - await Delay(300, ct); + using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.24), (int)(ra1.Width * 0.29))); + string affixes = GetArtifactAffixes(card.SrcMat, OcrFactory.Paddle); - using var ra1 = CaptureToRectArea(); - using ImageRegion grid1 = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.025), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.66), (int)(ra1.Width * 0.4))); - using ImageRegion itemRegion1 = grid1.DeriveCrop(item); - if (GetArtifactStatus(itemRegion1.SrcMat) == ArtifactStatus.Selected) + if (IsMatchRegularExpression(affixes, regularExpression, out string msg)) { - using ImageRegion card = ra1.DeriveCrop(new Rect((int)(ra1.Width * 0.70), (int)(ra1.Width * 0.055), (int)(ra1.Width * 0.24), (int)(ra1.Width * 0.29))); - string affixes = GetArtifactAffixes(card.SrcMat, OcrFactory.Paddle); - - if (checkedArtifactAffixesQueue.Any(c => c == affixes)) - { - duplicateSum++; - logger.LogInformation($"重复检查了该圣遗物"); - } - if (checkedArtifactAffixesQueue.Count >= 36) // 一个grid最多能看到36个完整的圣遗物 - { - checkedArtifactAffixesQueue.Dequeue(); - } - checkedArtifactAffixesQueue.Enqueue(affixes); - - if (IsMatchRegularExpression(affixes, regularExpression, out string msg)) - { - logger.LogInformation(message: msg); - } - else - { - itemRegion.Click(); - await Delay(100, ct); - } - if (duplicateSum >= 3) - { - break; - } - } - count--; - if (count <= 0) - { - break; - } - } - } - if (count <= 0 || duplicateSum >= 3) - { - break; - } - if (anyItemChecked) - { - for (int i = 0; i < 32; i++) // 先滚动大约三行半 - { - input.Mouse.VerticalScroll(-2); - await Delay(40, ct); - } - - DateTimeOffset rollingEndTime = DateTime.Now.AddSeconds(2); - while (DateTime.Now < rollingEndTime) - { - await Delay(60, ct); - using var ra2 = CaptureToRectArea(); - using ImageRegion grid2 = ra2.DeriveCrop(new Rect((int)(ra2.Width * 0.025), (int)(ra2.Width * 0.055), (int)(ra2.Width * 0.66), (int)(ra2.Width * 0.4))); - IEnumerable gridItems2 = GetArtifactGridItems(grid2.SrcMat); - if (gridItems2.Min(i => i.Y) > (ra2.Width * 0.018)) // 精细滚动,保证完整地显示四行 - { - input.Mouse.VerticalScroll(-1); + logger.LogInformation(message: msg); } else { - break; + itemRegion.Click(); + await Delay(100, ct); } } - - grid.MoveTo(grid.Width, grid.Height); - await Delay(500, ct); + count--; + if (count <= 0) + { + logger.LogInformation("检查次数已耗尽"); + break; + } } - else - { - await Delay(400, ct); - logger.LogInformation("找不到可检查的圣遗物了"); - break; - } - } - if (count <= 0) - { - logger.LogInformation("检查次数已耗尽"); - } - if (duplicateSum >= 3) - { - logger.LogInformation("重复检查次数过多,推断为找不到可检查的了"); } } @@ -352,33 +277,6 @@ public class AutoArtifactSalvageTask : ISoloTask return ocrResult.Text; } - public static IEnumerable GetArtifactGridItems(Mat src) - { - using Mat grey = src.CvtColor(ColorConversionCodes.BGR2GRAY); - - using Mat canny = grey.Canny(20, 40); - - Cv2.FindContours(canny, out var contours, out _, RetrievalModes.External, - ContourApproximationModes.ApproxSimple, null); - - IEnumerable boxes = contours.Where(c => Cv2.MinAreaRect(c).Angle % 90 <= 1) // 剔除倾斜 - .Select(Cv2.BoundingRect).Where(r => - { - if (r.Height == 0) - { - return false; - } - return Math.Abs((float)r.Width / r.Height - 0.8) < 0.05; // 按形状筛选 - }).ToList(); - - //src.DrawContours(contours, -1, Scalar.Red); - - int biggestRectHeight = boxes.Max(b => b.Height); - boxes = boxes.Where(b => (float)b.Height / biggestRectHeight > 0.88); // 剔除太小的 - - return boxes.ToArray(); - } - public static ArtifactStatus GetArtifactStatus(Mat src) { using Mat upperLine = new Mat(src, new Rect(0, 0, src.Width, (int)(src.Height * 0.19))); diff --git a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainConfig.cs b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainConfig.cs index f828fa0d..cdd0d3be 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainConfig.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using System; +using System.Collections.Generic; namespace BetterGenshinImpact.GameTask.AutoDomain; @@ -51,4 +52,32 @@ public partial class AutoDomainConfig : ObservableObject // 周日奖励序号 [ObservableProperty] private string _sundaySelectedValue = string.Empty; + + // 指定树脂的使用次数 + [ObservableProperty] + private bool _specifyResinUse = false; + + // 自定义使用树脂优先级 + [ObservableProperty] + private List _resinPriorityList = + [ + "浓缩树脂", + "原粹树脂" + ]; + + // 使用原粹树脂刷取副本次数 + [ObservableProperty] + private int _originalResinUseCount = 0; + + //使用浓缩树脂刷取副本次数 + [ObservableProperty] + private int _condensedResinUseCount = 0; + + // 使用须臾树脂刷取副本次数 + [ObservableProperty] + private int _transientResinUseCount = 0; + + // 使用脆弱树脂刷取副本次数 + [ObservableProperty] + private int _fragileResinUseCount = 0; } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs index a14e869c..8faa8911 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainParam.cs @@ -1,4 +1,5 @@ -using BetterGenshinImpact.GameTask.Model; +using System.Collections.Generic; +using BetterGenshinImpact.GameTask.Model; using System.Threading; namespace BetterGenshinImpact.GameTask.AutoDomain; @@ -25,6 +26,27 @@ public class AutoDomainParam : BaseTaskParam // 1~4 public string MaxArtifactStar { get; set; } = "4"; + public bool SpecifyResinUse { get; set; } = false; + + // 使用树脂优先级 + public List ResinPriorityList { get; set; } = + [ + "浓缩树脂", + "原粹树脂" + ]; + + // 使用原粹树脂刷取副本次数 + public int OriginalResinUseCount { get; set; } = 0; + + // 使用浓缩树脂刷取副本次数 + public int CondensedResinUseCount { get; set; } = 0; + + // 使用须臾树脂刷取副本次数 + public int TransientResinUseCount { get; set; } = 0; + + // 使用脆弱树脂刷取副本次数 + public int FragileResinUseCount { get; set; } = 0; + public AutoDomainParam(int domainRoundNum, string path) { DomainRoundNum = domainRoundNum; @@ -45,5 +67,11 @@ public class AutoDomainParam : BaseTaskParam SundaySelectedValue = config.SundaySelectedValue; AutoArtifactSalvage = config.AutoArtifactSalvage; MaxArtifactStar = TaskContext.Instance().Config.AutoArtifactSalvageConfig.MaxArtifactStar; + ResinPriorityList = config.ResinPriorityList; + OriginalResinUseCount = config.OriginalResinUseCount; + CondensedResinUseCount = config.CondensedResinUseCount; + TransientResinUseCount = config.TransientResinUseCount; + FragileResinUseCount = config.FragileResinUseCount; + SpecifyResinUse = config.SpecifyResinUse; } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs index 2a690ade..2d368aa8 100644 --- a/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs +++ b/BetterGenshinImpact/GameTask/AutoDomain/AutoDomainTask.cs @@ -39,6 +39,8 @@ using System.Text.RegularExpressions; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using System.Collections.ObjectModel; using BetterGenshinImpact.Core.Script.Dependence; +using BetterGenshinImpact.GameTask.AutoDomain.Model; +using BetterGenshinImpact.GameTask.Common; using Compunet.YoloSharp; using Microsoft.Extensions.DependencyInjection; @@ -69,6 +71,8 @@ public class AutoDomainTask : ISoloTask private readonly string matchingChallengeString; private readonly string rapidformationString; + private List _resinPriorityListWhenSpecifyUse; + public AutoDomainTask(AutoDomainParam taskParam) { AutoFightAssets.DestroyInstance(); @@ -79,6 +83,8 @@ public class AutoDomainTask : ISoloTask _combatScriptBag = CombatScriptParser.ReadAndParse(_taskParam.CombatStrategyPath); + _resinPriorityListWhenSpecifyUse = ResinUseRecord.BuildFromDomainParam(taskParam); + IStringLocalizer stringLocalizer = App.GetService>() ?? throw new NullReferenceException(); CultureInfo cultureInfo = new CultureInfo(TaskContext.Instance().Config.OtherConfig.GameCultureInfoName); @@ -182,17 +188,9 @@ public class AutoDomainTask : ISoloTask // 5. 快速领取奖励并判断是否有下一轮 Logger.LogInformation("自动秘境:{Text}", "5. 领取奖励"); - if (!GettingTreasure(_taskParam.DomainRoundNum == 9999, i == _taskParam.DomainRoundNum - 1)) + if (!await GettingTreasure()) { - if (i == _taskParam.DomainRoundNum - 1) - { - Logger.LogInformation("配置的{Cnt}轮秘境已经完成,结束自动秘境", _taskParam.DomainRoundNum); - } - else - { - Logger.LogInformation("体力已经耗尽,结束自动秘境"); - } - + Logger.LogInformation("体力耗尽或者设置轮次已达标,结束自动秘境"); break; } @@ -400,84 +398,58 @@ public class AutoDomainTask : ISoloTask Logger.LogInformation("周日未设置秘境奖励序号,不进行奖励选择"); } } - else - { - Logger.LogWarning("周日设置秘境奖励序号错误,请检查配置页面"); - } } else { - Logger.LogWarning("周日奖励选择:圣遗物副本无需选择奖励"); + Logger.LogDebug("周日奖励选择:圣遗物副本无需选择奖励"); } await Delay(300, _ct); //await Delay(100000, _ct);//调试延时========= } - // 点击单人挑战,增加容错,点击失败则继续尝试 + // 点击单人挑战 int retryTimes = 0; - while (retryTimes < 40) + while (retryTimes < 20) { retryTimes++; using var confirmRectArea = CaptureToRectArea().Find(fightAssets.ConfirmRa); if (!confirmRectArea.IsEmpty()) { - await Delay(500, _ct); confirmRectArea.Click(); - await Delay(500, _ct); - var ra = CaptureToRectArea(); - var matchingChallengeArea = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.64, ra.Height * 0.91, - ra.Width * 0.13, ra.Height * 0.06)); - var done = matchingChallengeArea.LastOrDefault(t => - Regex.IsMatch(t.Text, this.matchingChallengeString)); - if (done != null) - { - continue; - } - else - { - break; - } - } - - await Delay(500, _ct); - } - - //如果卡顿,可能会错过"是否仍要挑战该秘境"判断弹框,改为判断"快速编队"后进行点击进入 - retryTimes = 0; - while (retryTimes < 30) - { - await Delay(600, _ct); - var ra = CaptureToRectArea(); - var rapidformationStringArea = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.64, ra.Height * 0.91, - ra.Width * 0.13, ra.Height * 0.06)); - var done = rapidformationStringArea.LastOrDefault(t => - Regex.IsMatch(t.Text, this.rapidformationString)); - if (done != null) - { - using var confirmRectArea = CaptureToRectArea().Find(fightAssets.ConfirmRa); - if (!confirmRectArea.IsEmpty()) - { - confirmRectArea.Click(); - await Delay(500, _ct); - } - } - else - { break; } - using var confirmRectArea2 = ra.Find(RecognitionObject.Ocr(ra.Width * 0.263, ra.Height * 0.32, - ra.Width - ra.Width * 0.263 * 2, ra.Height - ra.Height * 0.32 - ra.Height * 0.353)); - if (confirmRectArea2.IsExist() && confirmRectArea2.Text.Contains("是否仍要挑战该秘境")) + await Delay(1500, _ct); + } + + // 判断弹框 + await Delay(600, _ct); + var ra = CaptureToRectArea(); + using var confirmRectArea2 = ra.Find(RecognitionObject.Ocr(ra.Width * 0.263, ra.Height * 0.32, + ra.Width - ra.Width * 0.263 * 2, ra.Height - ra.Height * 0.32 - ra.Height * 0.353)); + if (confirmRectArea2.IsExist() && confirmRectArea2.Text.Contains("是否仍要挑战该秘境")) + { + Logger.LogWarning("自动秘境:检测到树脂不足提示:{Text}", confirmRectArea2.Text); + throw new Exception("当前树脂不足,自动秘境停止运行。"); + } + + // 点击进入 + retryTimes = 0; + while (retryTimes < 20) + { + retryTimes++; + using var confirmRectArea = CaptureToRectArea().Find(fightAssets.ConfirmRa); + if (!confirmRectArea.IsEmpty()) { - Logger.LogWarning("自动秘境:检测到树脂不足提示:{Text}", confirmRectArea2.Text); - throw new Exception("当前树脂不足,自动秘境停止运行。"); + confirmRectArea.Click(); + break; } - retryTimes++; + await Delay(1200, _ct); } + // 载入动画 await Delay(3000, _ct); } @@ -615,6 +587,7 @@ public class AutoDomainTask : ISoloTask finally { Logger.LogInformation("自动战斗线程结束"); + Simulation.ReleaseAllKey(); } }, cts.Token); @@ -997,7 +970,7 @@ public class AutoDomainTask : ISoloTask Simulation.SendInput.Mouse.MoveMouseBy(moveAngle, 0); } - Sleep(100); + Sleep(100, _ct); } Logger.LogInformation("锁定东方向视角线程结束"); @@ -1008,64 +981,131 @@ public class AutoDomainTask : ISoloTask /// /// 领取奖励 /// - /// 是否识别树脂 - /// 是否最后一轮 - private bool GettingTreasure(bool recognizeResin, bool isLastTurn) + private async Task GettingTreasure() { + bool isLastTurn = false; // 等待窗口弹出 - Sleep(1500, _ct); + await Delay(300, _ct); - // 优先使用浓缩树脂 - var retryTimes = 0; - while (true) + // 1. OCR 直到确认弹出框弹出 + bool chooseResinPrompt = await NewRetry.WaitForAction(() => { - retryTimes++; - if (retryTimes > 3) + using var ra = CaptureToRectArea(); + var regionList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6)); + var res = regionList.FirstOrDefault(t => t.Text.Contains("石化古树")); + if (res != null) { - Logger.LogInformation("没有浓缩树脂了"); - break; + // 解决水龙王按下左键后没松开,然后后续点击按下就没反应了,界面上点一下 + res.Click(); + return true; } - var useCondensedResinRa = CaptureToRectArea().Find(AutoFightAssets.Instance.UseCondensedResinRa); - if (!useCondensedResinRa.IsEmpty()) - { - useCondensedResinRa.Click(); - // 点两下 #224 #218 - // 解决水龙王按下左键后没松开,然后后续点击按下就没反应了 - Sleep(400, _ct); - useCondensedResinRa.Click(); - break; - } + return false; + }, _ct, 10, 500); + Debug.WriteLine("识别到选择树脂页"); + await Delay(800, _ct); - Sleep(800, _ct); + // 再 OCR 一次,弹出框,确认当前是否有原粹树脂 + using var ra2 = CaptureToRectArea(); + var textListInPrompt = ra2.FindMulti(RecognitionObject.Ocr(ra2.Width * 0.25, ra2.Height * 0.2, ra2.Width * 0.5, ra2.Height * 0.6)); + if (textListInPrompt.Any(t => t.Text.Contains("数量不足") || t.Text.Contains("补充原粹树脂"))) + { + // 没有原粹树脂,直接退出秘境 + Logger.LogInformation("自动秘境:原粹树脂已用尽,退出秘境"); + await ExitDomain(); + return false; + } + + if (chooseResinPrompt) + { + using var ra3 = CaptureToRectArea(); + + if (!_taskParam.SpecifyResinUse) + { + // 自动刷干树脂 + // 识别树脂状况 + var resinStatus = ResinStatus.RecogniseFromRegion(ra3); + resinStatus.Print(Logger); + + if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) + { + Logger.LogWarning("树脂不足"); + await ExitDomain(); + return false; + } + + bool resinUsed = false; + if (resinStatus.CondensedResinCount > 0) + { + resinUsed = PressUseResin(ra3, "浓缩树脂"); + resinStatus.CondensedResinCount -= 1; + } + else if (resinStatus.OriginalResinCount >= 20) + { + resinUsed = PressUseResin(ra3, "原粹树脂"); + resinStatus.OriginalResinCount -= 20; + } + + if (!resinUsed) + { + Logger.LogWarning("自动秘境:未找到可用的树脂,可能是{Msg1} 或者 {Msg2}。", "树脂不足", "OCR 识别失败"); + await ExitDomain(); + return false; + } + + if (resinStatus is { CondensedResinCount: <= 0, OriginalResinCount: < 20 }) + { + // 没树脂了就是最后一回合了 + isLastTurn = true; + } + } + else + { + // 指定使用树脂 + var textListInPrompt2 = ra3.FindMulti(RecognitionObject.Ocr(ra3.Width * 0.25, ra3.Height * 0.2, ra3.Width * 0.5, ra3.Height * 0.6)); + // 按优先级使用 + var failCount = 0; + foreach (var record in _resinPriorityListWhenSpecifyUse) + { + if (record.RemainCount > 0 && PressUseResin(textListInPrompt2, record.Name)) + { + record.RemainCount -= 1; + Logger.LogInformation("自动秘境:{Name} 刷取 {Re}/{Max}", record.Name, record.MaxCount - record.RemainCount, record.MaxCount); + break; + } + else + { + failCount++; + } + } + + if (_resinPriorityListWhenSpecifyUse.Sum(o => o.RemainCount) <= 0) + { + // 全部刷完 + isLastTurn = true; + } + + if (failCount == _resinPriorityListWhenSpecifyUse.Count) + { + // 没有找到对应的树脂 + Logger.LogWarning("自动秘境:指定树脂领取次数时,当前可用树脂选项无法满足配置。你可能设置的刷取次数过多!退出秘境。"); + Logger.LogInformation("当前刷取情况:{ResinList}", string.Join(", ", _resinPriorityListWhenSpecifyUse.Select(o => $"{o.Name}({o.MaxCount - o.RemainCount}/{o.MaxCount})"))); + await ExitDomain(); + return false; + } + } + } + else + { + // 如果没有选择树脂的提示,说明只有原粹树脂 + // 继续向下执行 } Sleep(1000, _ct); - var hasSkip = false; - var captureArea = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; - var assetScale = TaskContext.Instance().SystemInfo.AssetScale; for (var i = 0; i < 30; i++) { - // 跳过领取动画 - if (!hasSkip) - { - TaskContext.Instance().PostMessageSimulator.LeftButtonClick(); // 先随便点一个地方使得跳过出现 - } - using var ra = CaptureToRectArea(); - - // OCR识别是否有跳过 - var ocrList = ra.FindMulti(RecognitionObject.Ocr(captureArea.Width - 230 * assetScale, 0, - 230 * assetScale - 5, 80 * assetScale)); - var skipTextRa = ocrList.FirstOrDefault(t => Regex.IsMatch(t.Text, this.skipLocalizedString)); - if (skipTextRa != null) - { - hasSkip = true; - skipTextRa.Click(); // 有则点击 - } - - // 优先点击继续 using var confirmRectArea = ra.Find(AutoFightAssets.Instance.ConfirmRa); if (!confirmRectArea.IsEmpty()) @@ -1080,28 +1120,33 @@ public class AutoDomainTask : ISoloTask return false; } } - - if (!recognizeResin) - { - confirmRectArea.Click(); - return true; - } - - var (condensedResinCount, fragileResinCount) = GetRemainResinStatus(); - if (condensedResinCount == 0 && fragileResinCount < 20) - { - // 没有体力了退出 - var exitRectArea = ra.Find(AutoFightAssets.Instance.ExitRa); - if (!exitRectArea.IsEmpty()) - { - exitRectArea.Click(); - return false; - } - } else { + if (!chooseResinPrompt) + { + // TODO 前面没有弹框的情况下,意味着只有原粹树脂,要再识别一次右上角确认树脂余量,没有余量直接退出 + } + // 有体力继续 confirmRectArea.Click(); + await Delay(60, _ct); // 双击 + confirmRectArea.Click(); + + if (!chooseResinPrompt) + { + // 真没树脂了还有提示兜底 + await Delay(900, _ct); + var textListInNoResinPrompt = CaptureToRectArea().FindMulti(RecognitionObject.Ocr(ra2.Width * 0.25, ra2.Height * 0.2, ra2.Width * 0.5, ra2.Height * 0.6)); + if (textListInNoResinPrompt.Any(t => t.Text.Contains("是否仍要") && t.Text.Contains("挑战") && t.Text.Contains("秘境"))) + { + var cancelBtn = textListInNoResinPrompt.FirstOrDefault(t => t.Text.Contains("取消")); + if (cancelBtn != null) + { + cancelBtn.Click(); + return false; + } + } + } return true; } } @@ -1112,41 +1157,69 @@ public class AutoDomainTask : ISoloTask throw new NormalEndException("未检测到秘境结束,可能是背包物品已满。"); } - /// - /// 获取剩余树脂状态 - /// - private (int, int) GetRemainResinStatus() + private async Task ExitDomain() { - var condensedResinCount = 0; - var fragileResinCount = 0; + Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE); + await Delay(500, _ct); + Simulation.SendInput.Keyboard.KeyPress(VK.VK_ESCAPE); + await Delay(800, _ct); + Bv.ClickBlackConfirmButton(CaptureToRectArea()); + } - var ra = CaptureToRectArea(); - // 浓缩树脂 - var condensedResinCountRa = ra.Find(AutoFightAssets.Instance.CondensedResinCountRa); - if (!condensedResinCountRa.IsEmpty()) + private bool PressUseResin(ImageRegion ra, string resinName) + { + var regionList = ra.FindMulti(RecognitionObject.Ocr(ra.Width * 0.25, ra.Height * 0.2, ra.Width * 0.5, ra.Height * 0.6)); + return PressUseResin(regionList, resinName); + } + + private bool PressUseResin(List regionList, string resinName) + { + var resinKey = regionList.FirstOrDefault(t => t.Text.Contains(resinName)); + if (resinKey != null) { - // 图像右侧就是浓缩树脂数量 - var countArea = ra.DeriveCrop(condensedResinCountRa.X + condensedResinCountRa.Width, - condensedResinCountRa.Y, condensedResinCountRa.Width, condensedResinCountRa.Height); - // Cv2.ImWrite($"log/resin_{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:ffff")}.png", countArea.SrcGreyMat); - var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat); - condensedResinCount = StringUtils.TryParseInt(count); + // 找到树脂名称对应的按键,关键词为使用,是同一行的(高度相交) + var useList = regionList.Where(t => t.Text.Contains("使用")).ToList(); + if (useList.Count != 0) + { + // 找到使用按键 + var useKey = useList.FirstOrDefault(t => t.X > TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect.Width / 2 + && IsHeightOverlap(t, resinKey)); + if (useKey != null) + { + // 点击使用 + useKey.Click(); + // 解决水龙王按下左键后没松开,然后后续点击按下就没反应了。使用双击 + Sleep(60, _ct); + useKey.Click(); + Logger.LogInformation("自动秘境:使用 {ResinName}", resinName); + return true; + } + else + { + Logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName); + } + } + else + { + Logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName); + } } - // 脆弱树脂 - var fragileResinCountRa = ra.Find(AutoFightAssets.Instance.FragileResinCountRa); - if (!fragileResinCountRa.IsEmpty()) - { - // 图像右侧就是脆弱树脂数量 - var countArea = ra.DeriveCrop(fragileResinCountRa.X + fragileResinCountRa.Width, fragileResinCountRa.Y, - (int)(fragileResinCountRa.Width * 3), fragileResinCountRa.Height); - var count = OcrFactory.Paddle.Ocr(countArea.SrcMat); - fragileResinCount = StringUtils.TryParseInt(count); - } + return false; + } - Logger.LogInformation("剩余:浓缩树脂 {CondensedResinCount} 原粹树脂 {FragileResinCount}", condensedResinCount, - fragileResinCount); - return (condensedResinCount, fragileResinCount); + /// + /// 判断两个区域在垂直方向上是否有重叠 + /// + private bool IsHeightOverlap(Region region1, Region region2) + { + int region1Top = region1.Y; + int region1Bottom = region1.Y + region1.Height; + int region2Top = region2.Y; + int region2Bottom = region2.Y + region2.Height; + + // 检查区域是否在垂直方向上重叠 + return (region1Top <= region2Bottom && region1Bottom >= region2Top); } private async Task ArtifactSalvage() diff --git a/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs b/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs new file mode 100644 index 00000000..f5cf2650 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs @@ -0,0 +1,73 @@ +using System; +using BetterGenshinImpact.Core.Recognition.OCR; +using BetterGenshinImpact.GameTask.AutoFight.Assets; +using BetterGenshinImpact.GameTask.Model.Area; +using BetterGenshinImpact.Helpers; +using Microsoft.Extensions.Logging; +using OpenCvSharp; + +namespace BetterGenshinImpact.GameTask.AutoDomain.Model; + +public class ResinStatus +{ + /// + /// 原粹树脂(1自回体) + /// + public int OriginalResinCount { get; set; } = 0; + + /// + /// 脆弱树脂(60) + /// + public int FragileResinCount { get; set; } = 0; + + /// + /// 浓缩树脂(40) + /// + public int CondensedResinCount { get; set; } = 0; + + /// + /// 须臾树脂(60壶内购买) + /// + public int TransientResinCount { get; set; } = 0; + + public static ResinStatus RecogniseFromRegion(ImageRegion region) + { + var status = new ResinStatus(); + + // 1. 原粹树脂 起点 w-(256+100) ~ w-256 + var captureArea = TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect; + var assetScale = TaskContext.Instance().SystemInfo.AssetScale; + var originalResinTopIconRa = AutoFightAssets.Instance.OriginalResinTopIconRa; + var originalResinRes = region.Find(originalResinTopIconRa); + if (originalResinRes.IsEmpty()) + { + throw new Exception("未找到原粹树脂图标"); + } + + // 找出 icon 的位置 + 30 ~ w-267 就是原粹树脂的数字 + var originalResinCountRect = new Rect(originalResinRes.Right + 30, (int)(37 * assetScale), + captureArea.Width - (originalResinRes.Right + 30) - (int)(267 * assetScale), (int)(21 * assetScale)); + string cnt1 = OcrFactory.Paddle.OcrWithoutDetector(region.DeriveCrop(originalResinCountRect).SrcMat); + status.OriginalResinCount = StringUtils.TryExtractPositiveInt(cnt1, 0); + + // 2. 浓缩树脂 + var condensedResinRes = region.Find(AutoFightAssets.Instance.CondensedResinTopIconRa); + if (condensedResinRes.IsExist()) + { + // 找出 icon 的位置 + 25 ~ icon 的位置+45 就是浓缩树脂的数字,数字宽20 + var condensedResinCountRect = new Rect(condensedResinRes.Right + (int)(25 * assetScale), condensedResinRes.Y, (int)(20 * assetScale), condensedResinRes.Height); + string cnt40 = OcrFactory.Paddle.OcrWithoutDetector(region.DeriveCrop(condensedResinCountRect).SrcMat); + status.CondensedResinCount = StringUtils.TryExtractPositiveInt(cnt40, 0); + } + + return status; + } + + public void Print(ILogger logger) + { + // logger.LogInformation("原粹树脂:{Cnt1},浓缩树脂:{Cnt2},须臾树脂:{Cnt3},脆弱树脂:{Cnt4}", + // OriginalResinCount, CondensedResinCount, FragileResinCount, TransientResinCount); + logger.LogInformation("原粹树脂:{Cnt1},浓缩树脂:{Cnt2}", + OriginalResinCount, CondensedResinCount); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinUseRecord.cs b/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinUseRecord.cs new file mode 100644 index 00000000..f096a135 --- /dev/null +++ b/BetterGenshinImpact/GameTask/AutoDomain/Model/ResinUseRecord.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace BetterGenshinImpact.GameTask.AutoDomain.Model; + +/// +/// 树脂使用记录 +/// 用于自动秘境指定树脂刷取次数时候,计算剩余刷取次数 +/// +public class ResinUseRecord +{ + public string Name { get; set; } + + public int RemainCount { get; set; } + + public int MaxCount { get; set; } + + public ResinUseRecord(string name, int maxCount) + { + Name = name; + RemainCount = maxCount; + MaxCount = maxCount; + } + + public static List BuildFromDomainParam(AutoDomainParam taskParam) + { + List list = []; + if (taskParam.SpecifyResinUse) + { + if (taskParam.CondensedResinUseCount > 0) + { + list.Add(new ResinUseRecord("浓缩树脂", taskParam.CondensedResinUseCount)); + } + if (taskParam.OriginalResinUseCount > 0) + { + list.Add(new ResinUseRecord("原粹树脂", taskParam.OriginalResinUseCount)); + } + if (taskParam.TransientResinUseCount > 0) + { + list.Add(new ResinUseRecord("须臾树脂", taskParam.TransientResinUseCount)); + } + if (taskParam.FragileResinUseCount > 0) + { + list.Add(new ResinUseRecord("脆弱树脂", taskParam.FragileResinUseCount)); + } + + if (list.Count == 0) + { + throw new Exception("你选择了指定树脂刷取次数,请至少配置一种树脂的刷取次数!"); + } + } + + return list; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/condensed_resin_top_icon.png b/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/condensed_resin_top_icon.png new file mode 100644 index 00000000..ef305664 Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/condensed_resin_top_icon.png differ diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/original_resin_top_icon.png b/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/original_resin_top_icon.png new file mode 100644 index 00000000..c7be1c1c Binary files /dev/null and b/BetterGenshinImpact/GameTask/AutoFight/Assets/1920x1080/original_resin_top_icon.png differ diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs b/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs index eb36651c..b600a9a4 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/Assets/AutoFightAssets.cs @@ -26,8 +26,11 @@ public class AutoFightAssets : BaseAssets // 树脂状态 public RecognitionObject CondensedResinCountRa; - public RecognitionObject FragileResinCountRa; + // 自动秘境 + // public RecognitionObject LockIconRa; // 锁定辅助图标 + public RecognitionObject CondensedResinTopIconRa; + public RecognitionObject OriginalResinTopIconRa; public Dictionary AvatarCostumeMap; @@ -245,5 +248,31 @@ public class AutoFightAssets : BaseAssets RegionOfInterest = new Rect(CaptureRect.Width / 2, CaptureRect.Height / 3 * 2, CaptureRect.Width / 2, CaptureRect.Height / 3), DrawOnWindow = false }.InitTemplate(); + + // 自动秘境 + // LockIconRa = new RecognitionObject + // { + // Name = "LockIcon", + // RecognitionType = RecognitionTypes.TemplateMatch, + // TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "lock_icon.png"), + // RegionOfInterest = new Rect(CaptureRect.Width - (int)(215 * AssetScale), 0, (int)(215 * AssetScale), (int)(80 * AssetScale)), + // DrawOnWindow = false + // }.InitTemplate(); + CondensedResinTopIconRa = new RecognitionObject + { + Name = "CondensedResinTopIcon", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "condensed_resin_top_icon.png"), + RegionOfInterest = new Rect((int)(1270 * AssetScale), (int)(25 * AssetScale), (int)(520 * AssetScale), (int)(45 * AssetScale)), + DrawOnWindow = false + }.InitTemplate(); + OriginalResinTopIconRa = new RecognitionObject + { + Name = "OriginalResinTopIcon", + RecognitionType = RecognitionTypes.TemplateMatch, + TemplateImageMat = GameTaskManager.LoadAssetImage("AutoFight", "original_resin_top_icon.png"), + RegionOfInterest = new Rect(CaptureRect.Width - (int)(450 * AssetScale), (int)(25 * AssetScale), (int)(265 * AssetScale), (int)(45 * AssetScale)), + DrawOnWindow = false + }.InitTemplate(); } } diff --git a/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json b/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json index cba93e16..ee0114de 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json +++ b/BetterGenshinImpact/GameTask/AutoFight/Assets/combat_avatar.json @@ -1868,5 +1868,35 @@ "nameEn": "Ifa", "skillCD": 7.5, "weapon": "10" + }, + { + "alias": [ + "丝柯克", + "Skirk", + "丝柯可", + "斯柯克" + ], + "burstCD": 15, + "id": "10000114", + "name": "丝柯克", + "nameEn": "SkirkNew", + "skillCD": 8, + "weapon": "1" + }, + { + "alias": [ + "塔利雅", + "Dahlia", + "塔利亚", + "塔丽雅", + "主祭", + "助祭" + ], + "burstCD": 15, + "id": "10000115", + "name": "塔利雅", + "nameEn": "Dahlia", + "skillCD": 9, + "weapon": "1" } ] \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs index ce34d262..127425f3 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightConfig.cs @@ -96,6 +96,11 @@ public partial class AutoFightConfig : ObservableObject [ObservableProperty] private int _pickDropsAfterFightSeconds = 15; + /// + /// 拾取战斗人次阈值,当战斗人次小于一定次数,就结束战斗情况下,不触发拾取掉落物和万叶拾取后拾取,只有不小于2时才生效。 + /// + [ObservableProperty] + private int? _battleThresholdForLoot; /// /// 战斗结束后,如果存在枫原万叶,则使用该角色捡材料 /// diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs index afa11bf6..5a5fb090 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightParam.cs @@ -36,6 +36,7 @@ public class AutoFightParam : BaseTaskParam FinishDetectConfig.BeforeDetectDelay = autoFightConfig.FinishDetectConfig.BeforeDetectDelay; KazuhaPartyName = autoFightConfig.KazuhaPartyName; OnlyPickEliteDropsMode = autoFightConfig.OnlyPickEliteDropsMode; + BattleThresholdForLoot = autoFightConfig.BattleThresholdForLoot ?? BattleThresholdForLoot; //下面参数固定,只取自动战斗里面的 FinishDetectConfig.BattleEndProgressBarColor = TaskContext.Instance().Config.AutoFightConfig.FinishDetectConfig.BattleEndProgressBarColor; FinishDetectConfig.BattleEndProgressBarColorTolerance = TaskContext.Instance().Config.AutoFightConfig.FinishDetectConfig.BattleEndProgressBarColorTolerance; @@ -48,7 +49,7 @@ public class AutoFightParam : BaseTaskParam public bool FightFinishDetectEnabled { get; set; } = false; public bool PickDropsAfterFightEnabled { get; set; } = false; public int PickDropsAfterFightSeconds { get; set; } = 15; - + public int BattleThresholdForLoot { get; set; } = -1; public int Timeout { get; set; } = 120; public bool KazuhaPickupEnabled = true; diff --git a/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs b/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs index f74a1ca6..410f2e37 100644 --- a/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs +++ b/BetterGenshinImpact/GameTask/AutoFight/AutoFightTask.cs @@ -417,44 +417,12 @@ public class AutoFightTask : ISoloTask }, cts2.Token); await fightTask; - - // 战斗结束检测线程 - // var endTask = Task.Run(async () => - // { - // if (!_taskParam.FightFinishDetectEnabled) - // { - // return; - // } - // - // try - // { - // while (!cts2.IsCancellationRequested) - // { - // var finish = await CheckFightFinish(); - // if (finish) - // { - // await cts2.CancelAsync(); - // break; - // } - // - // Sleep(1000, cts2.Token); - // } - // } - // catch (Exception e) - // { - // Debug.WriteLine(e.Message); - // Debug.WriteLine(e.StackTrace); - // } - // }, cts2.Token); - // - // await Task.WhenAll(fightTask, endTask); - //战斗人次少于2时(通常无怪物情况下),跳过拾取 - if (countFight < 2) + if (_taskParam.BattleThresholdForLoot>=2 && countFight < _taskParam.BattleThresholdForLoot) { + Logger.LogInformation($"战斗人次({countFight})低于配置人次({_taskParam.BattleThresholdForLoot}),跳过此次拾取!"); return; } - if (_taskParam.KazuhaPickupEnabled) { // 队伍中存在万叶的时候使用一次长E @@ -505,7 +473,7 @@ public class AutoFightTask : ISoloTask } else { - Logger.LogInformation((countFight < 2 ? "首个人出招就结束战斗,应该无怪物" : "距最近一次万叶出招,时间过短") + ",跳过此次万叶拾取!"); + Logger.LogInformation("距最近一次万叶出招,时间过短,跳过此次万叶拾取!"); } } //切换过队伍的,需要再切回来 diff --git a/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs b/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs index 6408cae3..fdbab99d 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/Model/BigFishType.cs @@ -21,15 +21,15 @@ public class BigFishType // public static readonly BigFishType FormaloRay = new("formalo ray", "fake fly bait", "佛玛洛鳐"); // public static readonly BigFishType DivdaRay = new("divda ray", "fake fly bait", "迪芙妲鳐"); - public static readonly BigFishType Angler = new("angler", "sugardew bait", "角鲀", 8); - public static readonly BigFishType AxeMarlin = new("axe marlin", "sugardew bait", "斧枪鱼", 9); - public static readonly BigFishType HeartfeatherBass = new("heartfeather bass", "sour bait", "心羽鲈", 10); - public static readonly BigFishType MaintenanceMek = new("maintenance mek", "flashing maintenance mek bait", "维护机关", 11); - public static readonly BigFishType Unihornfish = new("unihornfish", "spinelgrain bait", "独角鱼", 11); - public static readonly BigFishType Sunfish = new("sunfish", "spinelgrain bait", "翻车鲀", 8); - public static readonly BigFishType Rapidfish = new("rapidfish", "spinelgrain bait", "斗士急流鱼", 10); - public static readonly BigFishType PhonyUnihornfish = new("phony unihornfish", "emberglow bait", "燃素独角鱼", 11); - public static readonly BigFishType MagmaRapidfish = new("magma rapidfish", "emberglow bait", "炽岩斗士急流鱼", 10); + public static readonly BigFishType Angler = new("angler", "sugardew bait", "角鲀", 7); + public static readonly BigFishType AxeMarlin = new("axe marlin", "sugardew bait", "斧枪鱼", 8); + public static readonly BigFishType HeartfeatherBass = new("heartfeather bass", "sour bait", "心羽鲈", 9); + public static readonly BigFishType MaintenanceMek = new("maintenance mek", "flashing maintenance mek bait", "维护机关", 10); + public static readonly BigFishType Unihornfish = new("unihornfish", "spinelgrain bait", "独角鱼", 10); + public static readonly BigFishType Sunfish = new("sunfish", "spinelgrain bait", "翻车鲀", 7); + public static readonly BigFishType Rapidfish = new("rapidfish", "spinelgrain bait", "斗士急流鱼", 9); + public static readonly BigFishType PhonyUnihornfish = new("phony unihornfish", "emberglow bait", "燃素独角鱼", 10); + public static readonly BigFishType MagmaRapidfish = new("magma rapidfish", "emberglow bait", "炽岩斗士急流鱼", 9); public static IEnumerable Values diff --git a/BetterGenshinImpact/GameTask/AutoFishing/RodInput.cs b/BetterGenshinImpact/GameTask/AutoFishing/RodInput.cs index cb33932e..597d473c 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/RodInput.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/RodInput.cs @@ -1,4 +1,6 @@ -namespace BetterGenshinImpact.GameTask.AutoFishing; +using static TorchSharp.torch; + +namespace BetterGenshinImpact.GameTask.AutoFishing; public record RodInput { diff --git a/BetterGenshinImpact/GameTask/AutoFishing/RodNet.cs b/BetterGenshinImpact/GameTask/AutoFishing/RodNet.cs index 86c1656e..3934977b 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/RodNet.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/RodNet.cs @@ -37,8 +37,14 @@ namespace BetterGenshinImpact.GameTask.AutoFishing; /// tmd 今天我意识到 XXXX可不就是XXXX /// /// 哦 到这一步以后剩下的就很弱智了 远了挪近一点 近了挪远一点 调调参差不多得了 +/// +/// *后来又新增了一些访谈内容: +/// +/// 额 总之就是要求不能把不咬钩的识别成咬钩的 但是咬钩的可以识别成不咬钩的 +/// +/// 然后就可视化一下onehot在不同距离的结果 加一个offset使得模型输出的结果在保证可以predict距离正好的结果的同时距离范围尽可能小 /// -public class RodNet : Module +public class RodNet : Module { const double alpha = 1734.34 / 2.5; // fitted parameters @@ -75,23 +81,21 @@ public class RodNet : Module static readonly double[] offset = { 0.8, 0.4, 0.35, 0.35, 0.6, 0.3, 0.3, 0.8, 0.8, 0.8, 0.8 }; - private readonly Module layers; + private Parameter thetaParameter; + private Parameter bParameter; + private Parameter dzParameter; + private Parameter hCoeffParameter; public RodNet() : base("RodNet") { - var weight = tensor(RodNet.weight, ScalarType.Float64); - var bias = tensor(RodNet.bias, ScalarType.Float64); + long num_embeddings = RodNet.weight.GetLength(0); + long embedding_dim = 3; - RodLayer1 rodLayer1 = new RodLayer1(num_embeddings: weight.shape[0], embedding_dim: weight.shape[1], input_dim: 3, output_dim: 3); - rodLayer1.SetWeightsManually(weight, bias); + this.thetaParameter = new Parameter(torch.randn(num_embeddings, embedding_dim, dtype: ScalarType.Float64)); + this.bParameter = new Parameter(torch.randn(num_embeddings, embedding_dim, dtype: ScalarType.Float64)); - var modules = new List<(string, Module)> - { - ($"rodLayer1", rodLayer1), - ($"softmax", nn.Softmax(1)) - }; - - layers = Sequential(modules); + this.dzParameter = new Parameter(torch.zeros(num_embeddings, 1, dtype: ScalarType.Float64)); + this.hCoeffParameter = new Parameter(torch.zeros(num_embeddings, 1, dtype: ScalarType.Float64)); RegisterComponents(); } @@ -109,58 +113,26 @@ public class RodNet : Module dst[i] /= sum; } } - public record NetInput(double dist, int fish_label); - public static NetInput? GeometryProcessing(RodInput input) + + internal static int GetRodState(RodInput input) { - double a, b, v0, u, v, h; + double[] pred = ComputeScores(input); - a = (input.rod_x2 - input.rod_x1) / 2 / alpha; - b = (input.rod_y2 - input.rod_y1) / 2 / alpha; - h = (input.fish_y2 - input.fish_y1) / 2 / alpha; + return Array.IndexOf(pred, pred.Max()); + } - if (a < b) - { - b = Math.Sqrt(a * b); - a = b + 1e-6; - } + public static double[] ComputeScores(RodInput input) + { + var (y0, z0, t, u, v, h) = GetRodStatePreProcess(input); - v0 = (288 - (input.rod_y1 + input.rod_y2) / 2) / alpha; - - u = (input.fish_x1 + input.fish_x2 - input.rod_x1 - input.rod_x2) / 2 / alpha; - v = (288 - (input.fish_y1 + input.fish_y2) / 2) / alpha; v -= h * h_coeff[input.fish_label]; - - double y0, z0, t; double x, y, dist; - y0 = Math.Sqrt(Math.Pow(a, 4) - b * b + a * a * (1 - b * b + v0 * v0)) / (a * a); - z0 = b / (a * a); - t = a * a * (y0 * b + v0) / (a * a - b * b); - x = u * (z0 + dz[input.fish_label]) * Math.Sqrt(1 + t * t) / (t - v); y = (z0 + dz[input.fish_label]) * (1 + t * v) / (t - v); dist = Math.Sqrt(x * x + (y - y0) * (y - y0)); - return new NetInput(dist, input.fish_label); - } - - internal static int GetRodState(RodInput input) - { - NetInput? netInput = GeometryProcessing(input); - if (netInput is null) - { - return -1; - } - - double[] pred = ComputeScores(netInput); - - return Array.IndexOf(pred, pred.Max()); - } - - public static double[] ComputeScores(NetInput netInput) - { - double dist = netInput.dist; - int fish_label = netInput.fish_label; + int fish_label = input.fish_label; double[] logits = new double[3]; for (int i = 0; i < 3; i++) @@ -177,73 +149,113 @@ public class RodNet : Module internal int GetRodState_Torch(RodInput input) { - NetInput? netInput = GeometryProcessing(input); - if (netInput is null) - { - return -1; - } - - Tensor outputTensor = ComputeScores_Torch(netInput); + using var _ = no_grad(); + Tensor outputTensor = ComputeScores_Torch(input); var max = argmax(outputTensor); return (int)max.item(); } - public Tensor ComputeScores_Torch(NetInput netInput) + public Tensor ComputeScores_Torch(RodInput input) { - double dist = netInput.dist; - int fish_label = netInput.fish_label; + using var _ = no_grad(); + this.SetWeightsManually(); - Tensor inputTensor = cat([tensor(new double[,] { { dist } }, dtype: ScalarType.Float64), - tensor(new int[,] { {fish_label } }, dtype: ScalarType.Int32)]).T; - var outputTensor = forward(inputTensor); + var (y0, z0, t, u, v, h) = GetRodStatePreProcess(input); - outputTensor[0][0] = outputTensor[0][0] - RodNet.offset[fish_label]; + Tensor fishLabel = tensor(new double[] { input.fish_label }, dtype: ScalarType.Int32); + Tensor uv = tensor(new double[,] { { u, v } }, dtype: ScalarType.Float64); + Tensor y0z0t = tensor(new double[,] { { y0, z0, t } }, dtype: ScalarType.Float64); + Tensor h_ = tensor(new double[,] { { h } }, dtype: ScalarType.Float64); - return outputTensor; + var logits = forward(fishLabel, uv, y0z0t, h_); + var output = PostProcess(logits, fishLabel); + + return output; } - public override Tensor forward(Tensor input) + /// + /// 使用时直接赋值已知权重 + /// + public void SetWeightsManually() { - return layers.forward(input); - } -} - -public class RodLayer1 : Module -{ - private readonly Embedding embedding1; - private readonly Embedding embedding2; - private readonly Linear linear; - public RodLayer1(long num_embeddings, long embedding_dim, long input_dim, long output_dim) - : base("RodLinear") - { - embedding1 = torch.nn.Embedding(num_embeddings, embedding_dim); - embedding2 = torch.nn.Embedding(num_embeddings, embedding_dim); - linear = torch.nn.Linear(input_dim, output_dim); - - RegisterComponents(); + var weightTensor = tensor(RodNet.weight, ScalarType.Float64); + var biasTensor = tensor(RodNet.bias, ScalarType.Float64); + var dzTensor = tensor(RodNet.dz, ScalarType.Float64).reshape([RodNet.dz.Length, 1]); + var h_coeffTensor = tensor(RodNet.h_coeff, ScalarType.Float64).reshape([RodNet.h_coeff.Length, 1]); + this.thetaParameter = new Parameter(weightTensor); + this.bParameter = new Parameter(biasTensor); + this.dzParameter = new Parameter(dzTensor); + this.hCoeffParameter = new Parameter(h_coeffTensor); } - public void SetWeightsManually(Tensor weight, Tensor bias) + public override Tensor forward(Tensor fishLabel, Tensor uv, Tensor y0z0t, Tensor h) { - embedding1.weight = new Parameter(weight); - embedding2.weight = new Parameter(bias); + var uvSplit = uv.split([1, 1], dim: 1); + Tensor u = uvSplit[0]; + Tensor v = uvSplit[1]; + + var y0z0tSplit = y0z0t.split([1, 1, 1], dim: 1); + Tensor y0 = y0z0tSplit[0]; + Tensor z0 = y0z0tSplit[1]; + Tensor t = y0z0tSplit[2]; + + v = v - h * hCoeffParameter[fishLabel]; + + Tensor x, y, dist; + + var dz = dzParameter[fishLabel]; + x = u * (z0 + dz) * torch.sqrt(1 + t * t) / (t - v); + y = (z0 + dz) * (1 + t * v) / (t - v); + dist = torch.sqrt(x * x + (y - y0) * (y - y0)); + + Tensor logits = this.thetaParameter[fishLabel] * dist + this.bParameter[fishLabel]; + + return logits; } - public override Tensor forward(Tensor input) + public Tensor PostProcess(Tensor logits, Tensor fishLabel) { - var splitInput = input.split([1, 1], dim: 1); - var dist = splitInput[0]; - var fish_label = splitInput[1].to(ScalarType.Int32).flatten(); + var x_softmax = torch.nn.functional.softmax(logits, 1); - var embed1 = embedding1.forward(fish_label); - //Console.WriteLine(String.Join(",", embed1.data())); - var embed2 = embedding2.forward(fish_label); - //Console.WriteLine(String.Join(",", embed2.data())); + Tensor x_offset = tensor(fishLabel.data().Select(l => RodNet.offset[l]).ToArray()); - linear.weight = new Parameter(embed1.T); - linear.bias = new Parameter(embed2); + x_softmax[torch.arange(x_offset.shape[0]), 0] -= x_offset; + return x_softmax; + } - return linear.forward(dist); + /// + /// 根据rod和fish的坐标计算y0z0t、uv、h + /// + /// + /// y0, z0, t, u, v, h + public static (double, double, double, double, double, double) GetRodStatePreProcess(RodInput input) + { + /* + * 以下为hutaofisher代码中关于部分变量的意义的注释 + # uv: screen coordinate of bbox center of the fish + # abv0: rod shape and center coordinate in screen + */ + double a, b, v0, u, v, h; + + a = (input.rod_x2 - input.rod_x1) / 2 / alpha; + b = (input.rod_y2 - input.rod_y1) / 2 / alpha; + h = (input.fish_y2 - input.fish_y1) / 2 / alpha; + + if (a < b) + { + b = Math.Sqrt(a * b); + a = b + 1e-6; + } + v0 = (288 - (input.rod_y1 + input.rod_y2) / 2) / alpha; + u = (input.fish_x1 + input.fish_x2 - input.rod_x1 - input.rod_x2) / 2 / alpha; + v = (288 - (input.fish_y1 + input.fish_y2) / 2) / alpha; + double y0, z0, t; + + y0 = Math.Sqrt(Math.Pow(a, 4) - b * b + a * a * (1 - b * b + v0 * v0)) / (a * a); + z0 = b / (a * a); + t = a * a * (y0 * b + v0) / (a * a - b * b); + + return (y0, z0, t, u, v, h); } } \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json index 8b945b42..c5c01670 100644 --- a/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json +++ b/BetterGenshinImpact/GameTask/AutoGeniusInvokation/Assets/tcg_character_card.json @@ -919,6 +919,83 @@ } ] }, + { + "id": 1114, + "nameEn": "citlali", + "type": "character", + "name": "茜特菈莉", + "hp": 10, + "energy": 2, + "element": "冰元素", + "weapon": "法器", + "skills": [ + { + "nameEn": "shadowstealing_spirit_vessel", + "name": "宿灵捕影", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1101, + "nameEn": "cryo", + "type": "冰元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "dawnfrost_darkstar", + "name": "霜昼黑星", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1101, + "nameEn": "cryo", + "type": "冰元素", + "count": 3 + } + ] + }, + { + "nameEn": "edict_of_entwined_splendor", + "name": "诸曜饬令", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1101, + "nameEn": "cryo", + "type": "冰元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 2 + } + ] + }, + { + "nameEn": "songs_of_profound_mystery", + "name": "奥秘传唱", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 1201, "nameEn": "barbara", @@ -1354,7 +1431,7 @@ "nameEn": "candace", "type": "character", "name": "坎蒂丝", - "hp": 10, + "hp": 11, "energy": 2, "element": "水元素", "weapon": "长柄武器", @@ -1569,7 +1646,7 @@ "nameEn": "neuvillette", "type": "character", "name": "那维莱特", - "hp": 10, + "hp": 11, "energy": 2, "element": "水元素", "weapon": "法器", @@ -2853,6 +2930,77 @@ } ] }, + { + "id": 1315, + "nameEn": "mavuika", + "type": "character", + "name": "玛薇卡", + "hp": 10, + "energy": 0, + "element": "火元素", + "weapon": "双手剑", + "skills": [ + { + "nameEn": "flames_weave_life", + "name": "以火织命", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1103, + "nameEn": "pyro", + "type": "火元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "the_named_moment", + "name": "称名之刻", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1103, + "nameEn": "pyro", + "type": "火元素", + "count": 2 + } + ] + }, + { + "nameEn": "hour_of_burning_skies", + "name": "燔天之时", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1103, + "nameEn": "pyro", + "type": "火元素", + "count": 4 + } + ] + }, + { + "nameEn": "fighting_spirit", + "name": "战意", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 1401, "nameEn": "fischl", @@ -3142,7 +3290,7 @@ "nameEn": "beidou", "type": "character", "name": "北斗", - "hp": 10, + "hp": 11, "energy": 3, "element": "雷元素", "weapon": "双手剑", @@ -3332,7 +3480,7 @@ "id": 1104, "nameEn": "electro", "type": "雷元素", - "count": 4 + "count": 3 }, { "id": 1110, @@ -4464,6 +4612,83 @@ } ] }, + { + "id": 1511, + "nameEn": "chasca", + "type": "character", + "name": "恰斯卡", + "hp": 10, + "energy": 2, + "element": "风元素", + "weapon": "弓", + "skills": [ + { + "nameEn": "phantom_feather_flurry", + "name": "迷羽流击", + "skillTag": [ + "普通攻击" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 1 + }, + { + "id": 1109, + "nameEn": "unaligned_element", + "type": "无色元素", + "count": 2 + } + ] + }, + { + "nameEn": "spirit_reins_shadow_hunt", + "name": "灵缰追影", + "skillTag": [ + "元素战技" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 3 + } + ] + }, + { + "nameEn": "soul_reapers_fatal_round", + "name": "索魂命袭", + "skillTag": [ + "元素爆发" + ], + "cost": [ + { + "id": 1105, + "nameEn": "anemo", + "type": "风元素", + "count": 3 + }, + { + "id": 1110, + "nameEn": "energy", + "type": "充能", + "count": 2 + } + ] + }, + { + "nameEn": "shadowhunt_shell", + "name": "追影弹", + "skillTag": [ + "被动技能" + ], + "cost": [] + } + ] + }, { "id": 1601, "nameEn": "ningguang", @@ -4691,7 +4916,7 @@ "nameEn": "albedo", "type": "character", "name": "阿贝多", - "hp": 10, + "hp": 12, "energy": 2, "element": "岩元素", "weapon": "单手剑", @@ -5550,7 +5775,7 @@ "nameEn": "baizhu", "type": "character", "name": "白术", - "hp": 10, + "hp": 11, "energy": 2, "element": "草元素", "weapon": "法器", @@ -6579,7 +6804,7 @@ "nameEn": "hydro_hilichurl_rogue", "type": "character", "name": "丘丘水行游侠", - "hp": 10, + "hp": 11, "energy": 2, "element": "水元素", "weapon": "其他武器", diff --git a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs index f21751dd..bac9811d 100644 --- a/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs +++ b/BetterGenshinImpact/GameTask/AutoPick/AutoPickTrigger.cs @@ -198,7 +198,7 @@ public partial class AutoPickTrigger : ITaskTrigger if (config.OcrEngine == PickOcrEngineEnum.Yap.ToString()) { var textMat = new Mat(content.CaptureRectArea.CacheGreyMat, textRect); - text = _pickTextInference.Inference(TextInferenceFactory.PreProcessForInference(textMat)); + text = _pickTextInference.Inference(textMat); } else { diff --git a/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs b/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs index 4e1b7396..7826d586 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/ExitAndReloginJob.cs @@ -6,6 +6,7 @@ using BetterGenshinImpact.GameTask.AutoGeniusInvokation.Exception; using BetterGenshinImpact.GameTask.AutoWood.Assets; using BetterGenshinImpact.GameTask.AutoWood.Utils; using BetterGenshinImpact.GameTask.Common.BgiVision; +using BetterGenshinImpact.GameTask.Common.Element.Assets; using BetterGenshinImpact.GameTask.Model.Area; using Microsoft.Extensions.Logging; using Vanara.PInvoke; @@ -15,54 +16,46 @@ namespace BetterGenshinImpact.GameTask.Common.Job; public class ExitAndReloginJob { - private AutoWoodAssets _assets; + private AutoWoodAssets _assets = AutoWoodAssets.Instance; private readonly Login3rdParty _login3rdParty = new(); public async Task Start(CancellationToken ct) { - //============== 退出游戏流程 ============== Logger.LogInformation("退出至登录页面"); - _assets = AutoWoodAssets.Instance; SystemControl.FocusWindow(TaskContext.Instance().GameHandle); - Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); - await Delay(800, ct); - - // 菜单界面验证(带重试机制) - try - { - NewRetry.Do(() => - { - using var contentRegion = CaptureToRectArea(); - using var ra = contentRegion.Find(_assets.MenuBagRo); - if (ra.IsEmpty()) - { - // 未检测到菜单时再次发送ESC - Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE); - throw new RetryException("菜单界面验证失败"); - } - }, TimeSpan.FromSeconds(1.2), 5); // 1.2秒内重试5次 - } - catch - { - // 即使失败也继续退出流程 - } - // 点击退出按钮 - GameCaptureRegion.GameRegionClick((size, scale) => (50 * scale, size.Height - 50 * scale)); - await Delay(500, ct); + // 等待菜单界面出现 + await NewRetry.WaitForElementAppear( + _assets.MenuBagRo, + () => Simulation.SendInput.Keyboard.KeyPress(User32.VK.VK_ESCAPE), + ct, + 5, + 1200 + ); + + // 点击退出按钮并等待确认弹窗出现 + await NewRetry.WaitForElementAppear( + _assets.ConfirmRo, + () => GameCaptureRegion.GameRegionClick((size, scale) => (50 * scale, size.Height - 50 * scale)), + ct, + 5, + 800 + ); + + // 点击确认退出并等待确认弹窗消失 + await NewRetry.WaitForElementDisappear( + _assets.ConfirmRo, + () => { + using var cr = CaptureToRectArea(); + cr.Find(_assets.ConfirmRo, ra => { ra.Click(); ra.Dispose(); }); + }, + ct, + 5, + 1000 + ); + await Delay(1000, ct); - // 确认退出 - using var cr = CaptureToRectArea(); - cr.Find(_assets.ConfirmRo, ra => - { - ra.Click(); - ra.Dispose(); - }); - - await Delay(1000, ct); // 等待退出完成 - //============== 重新登录流程 ============== - // 第三方登录(如果启用) Logger.LogInformation("点击登录"); _login3rdParty.RefreshAvailabled(); if (_login3rdParty is { Type: Login3rdParty.The3rdPartyType.Bilibili, IsAvailabled: true }) @@ -72,45 +65,48 @@ public class ExitAndReloginJob Logger.LogInformation("退出重登启用 B 服模式"); } - // 进入游戏检测 - var clickCnt = 0; - for (var i = 0; i < 50; i++) + // 等待进入游戏按钮出现并点击 + var enterGameAppear = await NewRetry.WaitForElementAppear( + _assets.EnterGameRo, + () => { }, + ct, + 50, + 1000 + ); + if (enterGameAppear) { - await Delay(1, ct); - - using var contentRegion = CaptureToRectArea(); - using var ra = contentRegion.Find(_assets.EnterGameRo); - if (!ra.IsEmpty()) - { - clickCnt++; - GameCaptureRegion.GameRegion1080PPosClick(955, 666); - } - else - { - if (clickCnt > 2) - { - await Delay(5000, ct); - break; - } - } - - await Delay(1000, ct); + // 点击进入游戏按钮直到它消失 + await NewRetry.WaitForElementDisappear( + _assets.EnterGameRo, + () => GameCaptureRegion.GameRegion1080PPosClick(955, 666), + ct, + 10, + 1000 + ); } - - if (clickCnt == 0) + else { throw new RetryException("未检测进入游戏界面"); } - for (var i = 0; i < 50; i++) + // 等待主界面出现 + var mainUiFound = await NewRetry.WaitForElementAppear( + ElementAssets.Instance.PaimonMenuRo, + () => { }, + ct, + 50, + 1000 + ); + + if (mainUiFound) { - if (Bv.IsInMainUi(CaptureToRectArea())) - { - Logger.LogInformation("退出重新登录结束!"); - break; - } - await Delay(1000, ct); + Logger.LogInformation("退出重新登录结束!"); + } + else + { + Logger.LogWarning("未检测到主界面,登录可能未完成"); } await Delay(500, ct); } -} \ No newline at end of file +} + diff --git a/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs b/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs index e06f2464..140cfb47 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/GoToSereniteaPotTask.cs @@ -82,35 +82,40 @@ internal class GoToSereniteaPotTask // 进入 壶 await ChangeCountryForce("尘歌壶", ct); - - - // 若未找到 ElementAssets.Instance.SereniteaPotRo 就是已经在尘歌壶了 - var ra = CaptureToRectArea(); - //确定洞天名称 - var list = ra.FindMulti(new RecognitionObject - { - RecognitionType = RecognitionTypes.Ocr, - RegionOfInterest = new Rect((int)(ra.Width * 0.86), ra.Height*9/10, (int)(ra.Width * 0.073), (int)(ra.Height*0.04)) - }); - if (list.Count > 0) - { - dongTianName = list[0].Text; - Logger.LogInformation("领取尘歌壶奖励:{text}", "洞天名称:" + dongTianName); + var ra = CaptureToRectArea(); + for (int i = 0; i < 5; i++){ + ra = CaptureToRectArea(); + //确定洞天名称 + var list = ra.FindMulti(new RecognitionObject + { + RecognitionType = RecognitionTypes.Ocr, + RegionOfInterest = new Rect((int)(ra.Width * 0.86), ra.Height*9/10, (int)(ra.Width * 0.073), (int)(ra.Height*0.04)) + }); + if (list.Count > 0) + { + dongTianName = list[0].Text; + Logger.LogInformation("领取尘歌壶奖励:{text}", "洞天名称:" + dongTianName); + await Task.Delay(100, ct); + break; + } + else + { + dongTianName = ""; + Logger.LogInformation("领取尘歌壶奖励:{text}", "未识别到洞天名称"); + } + await Task.Delay(100, ct); } - else - { - dongTianName = ""; - Logger.LogInformation("领取尘歌壶奖励:{text}", "未识别到洞天名称"); - } - - for (int i = 0; i < 3; i++) + + for (int i = 0; i < 5; i++) { var sereniteaPotHomeIcon = ra.Find(ElementAssets.Instance.SereniteaPotHomeRo); if (!sereniteaPotHomeIcon.IsExist()) { - Logger.LogInformation("领取尘歌壶奖励:{text}", "住宅图标未找到,调整地图缩放至3.0。"); - await new Core.Script.Dependence.Genshin().SetBigMapZoomLevel(3.0); + Logger.LogInformation("领取尘歌壶奖励:{text}", "住宅图标未找到,调整地图缩放至2。"); + await Task.Delay(1000, ct); + await new Core.Script.Dependence.Genshin().SetBigMapZoomLevel(2.5-i*0.2);//尝试缩放地图 + await Task.Delay(1000, ct); } else { @@ -120,29 +125,44 @@ internal class GoToSereniteaPotTask } } - ra = CaptureToRectArea(); - var teleportBtn = ra.Find(QuickTeleportAssets.Instance.TeleportButtonRo); - if (!teleportBtn.IsExist()) + + for (int attempt = 0; attempt < 10; attempt++) // 尝试点击传送按钮 { + ra = CaptureToRectArea(); + var teleportBtn = ra.Find(QuickTeleportAssets.Instance.TeleportButtonRo); + if (teleportBtn.IsExist()) + { + teleportBtn.Click(); + break; // 找到并点击后退出循环 + } + var teleportSereniteaPotHome = ra.Find(ElementAssets.Instance.TeleportSereniteaPotHomeRo); if (teleportSereniteaPotHome.IsExist()) { teleportSereniteaPotHome.Click(); + break; // 找到并点击后退出循环 } + await Delay(500, ct); } - - ra = CaptureToRectArea(); - teleportBtn = ra.Find(QuickTeleportAssets.Instance.TeleportButtonRo); - if (teleportBtn.IsExist()) + + for (int i = 0; i < 10; i++)//有传送图标,点击传送 { - teleportBtn.Click(); + ra = CaptureToRectArea(); + var teleportBtn = ra.Find(QuickTeleportAssets.Instance.TeleportButtonRo); + if (teleportBtn.IsExist()) + { + teleportBtn.Click(); + await Delay(1000, ct); + } + else + { + break; + } } await NewRetry.WaitForAction(() => Bv.IsInMainUi(CaptureToRectArea()), ct); } - - // 寻找阿圆并靠近 private async Task FindAYuan(CancellationToken ct) { @@ -297,18 +317,18 @@ internal class GoToSereniteaPotTask if (numberBtn.IsExist()) { numberBtn.Move(); - await Delay(300, ct); + await Delay(600, ct);//减慢速度,设备差异导致的延迟 Simulation.SendInput.Mouse.LeftButtonDown(); - await Delay(300, ct); + await Delay(600, ct); numberBtn.MoveTo(ra.Width/7,0);//moveby会超出边界,改用MoveTo - await Delay(300, ct); + await Delay(600, ct); Simulation.SendInput.Mouse.LeftButtonUp(); } - await Delay(300, ct); - ra.Find(ElementAssets.Instance.BtnWhiteConfirm).Click(); - await Delay(500, ct); + await Delay(600, ct); ra.Find(ElementAssets.Instance.BtnWhiteConfirm).Click(); + await Delay(600, ct); + TaskContext.Instance().PostMessageSimulator.SimulateAction(GIActions.OpenPaimonMenu); // ESC } private async Task GetReward(CancellationToken ct) @@ -359,8 +379,8 @@ internal class GoToSereniteaPotTask Logger.LogInformation("领取尘歌壶奖励:{text}", "未配置购买商店物品"); return; } - DateTime now = DateTime.Now; - DayOfWeek currentDayOfWeek = now.DayOfWeek; + DateTime now = DateTime.Now; + DayOfWeek currentDayOfWeek = now.Hour >= 4 ? now.DayOfWeek : now.AddDays(-1).DayOfWeek; DayOfWeek? configDayOfWeek = GetDayOfWeekFromConfig(SelectedConfig.SecretTreasureObjects.First()); if (configDayOfWeek.HasValue || SelectedConfig.SecretTreasureObjects.First() == "每天重复" && SelectedConfig.SecretTreasureObjects.Count > 1) { @@ -371,7 +391,6 @@ internal class GoToSereniteaPotTask if (shopOption == TalkOptionRes.FoundAndClick) { Logger.LogInformation("领取尘歌壶奖励:{text}", "购买商店物品"); - await Delay(500, ct); // 购买的物品清单 var buy = new List(); @@ -410,18 +429,39 @@ internal class GoToSereniteaPotTask break; } } - + + //对比购买成功和buy的数量,如果不等,重试一次 + var buyCount = 0; + var retryBuy= 0; // 直接购买最大数量 - foreach (var item in buy) + while (retryBuy < 2) { - var itemRo = CaptureToRectArea().Find(item); - if (itemRo.IsExist()) + foreach (var item in buy) { - Logger.LogInformation("领取尘歌壶奖励:购买 {text} ", item.Name); - itemRo.Click(); - await Delay(600, ct); - await BuyMaxNumber(ct); - await Delay(1200, ct);//等待购买动画结束 + var itemRo = CaptureToRectArea().Find(item); + if (itemRo.IsExist()) + { + buyCount++; + Logger.LogInformation("领取尘歌壶奖励:购买 {text} ", item.Name); + itemRo.Click(); + await Delay(600, ct); + await BuyMaxNumber(ct); + await Delay(1000, ct);//等待购买动画结束 + } + else + { + await Delay(700, ct); + Logger.LogInformation("领取尘歌壶奖励: {text} 未找到", item.Name); + } + await Delay(700, ct); + } + if (buyCount < buy.Count) + { + retryBuy++; + await Delay(500, ct); + }else + { + break; } } await Delay(900, ct); diff --git a/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs b/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs index 12faf95d..3607ce23 100644 --- a/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs +++ b/BetterGenshinImpact/GameTask/Common/Job/SwitchPartyTask.cs @@ -51,11 +51,11 @@ public class SwitchPartyTask { Simulation.SendInput.SimulateAction(GIActions.OpenPartySetupScreen); - // 考虑加载时间 2s,共检查 2.5s,如果失败则抛出异常 + // 考虑加载时间 2s,共检查 3s,如果失败则抛出异常 - for (int i = 0; i < 3; i++) // 检查 3 次 + for (int i = 0; i < 5; i++) // 检查 5 次 { - await Delay(850, ct); + await Delay(600, ct); using var raCheck = CaptureToRectArea(); if (Bv.IsInPartyViewUi(raCheck)) { @@ -68,11 +68,6 @@ public class SwitchPartyTask { break; // 页面已打开,跳出循环 } - - if (attempt < maxAttempts) - { - Logger.LogWarning("尝试打开队伍配置页面失败,正在重试..."); - } } if (!isOpened) @@ -208,4 +203,4 @@ public class SwitchPartyTask await Delay(500, ct); await _returnMainUiTask.Start(ct); } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/BaseMapLayerByTemplateMatch.cs b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/BaseMapLayerByTemplateMatch.cs new file mode 100644 index 00000000..b07e02e3 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/BaseMapLayerByTemplateMatch.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +//using System.Diagnostics; +using System.IO; +//using System.Linq; +using System.Text.Json; +using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Helpers; +using OpenCvSharp; +using System.Text.Json.Serialization; +using BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; + +namespace BetterGenshinImpact.GameTask.Common.Map.Maps.Base; + +using static MiniMapMatchConfig; +public class BaseMapLayerByTemplateMatch +{ + public string LayerGroupId { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public float Scale { get; set; } = 1; + public int Floor { get; set; } = 0; + public float Top { get; set; } = 0; + public float Left { get; set; } = 0; + public bool IsOverSize { get; set; } = false; + [JsonIgnore] + public required FastSqDiffMatcher CoarseColorMatcher; // 小尺寸彩图 + [JsonIgnore] + public Mat FineGrayMap = new Mat(); // 大尺寸灰度图 + + public void LoadLayer(string layerDir) + { + SpeedTimer speedTimer = new($"加载 {LayerGroupId} 地图图片"); + var colorMapFileName = "color_" + LayerGroupId + ".webp"; + var colorMapPath = Path.Combine(layerDir, colorMapFileName); + var coarseColorMap = Cv2.ImRead(colorMapPath)?? throw new Exception($"彩色分层地图 {LayerGroupId} 读取失败"); + speedTimer.Record("精确匹配用彩图"); + CoarseColorMatcher = new FastSqDiffMatcher(coarseColorMap, new Size(52, 52)); + var grayMapFileName = "gray_" + LayerGroupId + (IsOverSize ? ".png" : ".webp"); + var grayMapPath = Path.Combine(layerDir, grayMapFileName); + FineGrayMap = Cv2.ImRead(grayMapPath, ImreadModes.Grayscale)?? throw new Exception($"灰度分层地图 {LayerGroupId} 读取失败"); + speedTimer.Record("粗匹配用灰度图"); + speedTimer.DebugPrint(); + } + + public static List LoadLayers(SceneBaseMapByTemplateMatch sceneBaseMap) + { + var layers = new List(); + var layerDir = Path.Combine(Global.Absolute(@"Assets\Map\"), sceneBaseMap.Type.ToString()); + if (!Directory.Exists(layerDir)) + { + return layers; + } + var jsonFiles = Directory.GetFiles(layerDir, "*.json", SearchOption.AllDirectories); + foreach (var jsonFile in jsonFiles) + { + var json = File.ReadAllText(jsonFile); + var tempLayers = JsonSerializer.Deserialize>(json) ?? throw new Exception("Failed to deserialize JSON."); + layers.AddRange(tempLayers); + } + foreach (var layer in layers) + { + layer.LoadLayer(layerDir); + } + return layers; + } + + public (Point2f, double) RoughMatch(Mat[] maskedMiniMaps, Mat maskF) + { + var (pos, val) = CoarseColorMatcher.Match(maskedMiniMaps, maskF); + return (MapToWorld(pos, RoughZoom, RoughSize), val); + } + + public (Point2f, double) RoughMatch(Mat[] maskedMiniMaps, Mat maskF, Point2f preLoc, int[]? channels = null) + { + var roughPos = WorldToMap(preLoc, RoughZoom); + var rect = GetRect(roughPos, (int)(RoughSearchRadius * Scale), RoughSize).Intersect(new Rect(0, 0, CoarseColorMatcher.Source[0].Width, CoarseColorMatcher.Source[0].Height)); + if (rect.Width < RoughSize || rect.Height < RoughSize) + { + return (default, -1); + } + var (pos, val) = CoarseColorMatcher.Match(maskedMiniMaps, maskF, rect, channels); + return (MapToWorld(rect.TopLeft + pos, RoughZoom, RoughSize), val); + } + + // 精确匹配直接返回世界坐标 + public (Point2f, double) ExactMatch(Mat miniMap, Mat mask, Point2f preLoc, TemplateMatchModes mode = TemplateMatchModes.SqDiff) + { + var exactPos = WorldToMap(preLoc, ExactZoom); + var rect = GetRect(exactPos, ExactSearchRadius, ExactSize).Intersect(new Rect(0, 0, FineGrayMap.Width, FineGrayMap.Height)); + if (rect.Width < ExactSize || rect.Height < ExactSize) + { + return (new Point2f(0, 0), -1); + } + var bigMap = FineGrayMap[rect]; + var (pos, val) = TemplateMatchHelper.MatchTemplateSubPix(bigMap, miniMap, mode, mask); + return (MapToWorld( rect.TopLeft + pos, ExactZoom, ExactSize), val); + } + + private static Rect GetRect(Point loc, int halfSide, int miniMapSize) + { + return new Rect(loc.X - halfSide - miniMapSize / 2, loc.Y - halfSide - miniMapSize / 2, halfSide * 2 + miniMapSize, halfSide * 2 + miniMapSize); + } + + private Point WorldToMap(Point2f pos, float zoom) + { + return new Point((int)Math.Round((pos.X / GlobalScale - Left) * Scale / zoom), (int)Math.Round((pos.Y / GlobalScale - Top) * Scale / zoom)); + } + private Point2f MapToWorld(Point2f pos, float zoom, int miniMapSize) + { + return new Point2f(GlobalScale * ((pos.X + miniMapSize / 2.0f) * zoom / Scale + Left), GlobalScale * ( (pos.Y + miniMapSize / 2.0f ) * zoom / Scale + Top)); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMap.cs b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMap.cs index 200153d1..dd535ab8 100644 --- a/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMap.cs +++ b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMap.cs @@ -95,7 +95,7 @@ public abstract class SceneBaseMap : ISceneMap return SiftMatcher.KnnMatchRect(MainLayer.TrainKeyPoints, MainLayer.TrainDescriptors, greyBigMapMat); } - public Point2f GetMiniMapPosition(Mat greyMiniMapMat) + public virtual Point2f GetMiniMapPosition(Mat greyMiniMapMat) { // 从表到里逐层匹配 foreach (var layer in Layers) @@ -117,7 +117,7 @@ public abstract class SceneBaseMap : ISceneMap return default; } - public Point2f GetMiniMapPosition(Mat greyMiniMapMat, float prevX, float prevY) + public virtual Point2f GetMiniMapPosition(Mat greyMiniMapMat, float prevX, float prevY) { if (prevX <= 0 && prevY <= 0) { diff --git a/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMapByTemplateMatch.cs b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMapByTemplateMatch.cs new file mode 100644 index 00000000..887fb48c --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/Map/Maps/Base/SceneBaseMapByTemplateMatch.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Eventing.Reader; +using System.IO; +using System.Linq; +using System.Text.Json; +using BetterGenshinImpact.Core.Recognition.OpenCv; +using BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch; +using OpenCvSharp; +using BetterGenshinImpact.GameTask.Common.Map.MiniMap; + +namespace BetterGenshinImpact.GameTask.Common.Map.Maps.Base; + +using static MiniMapMatchConfig; + +public abstract class SceneBaseMapByTemplateMatch : SceneBaseMap +{ + private readonly MiniMapPreprocessor _miniMapPreprocessor = new(); + + public new List Layers { get; set; } = []; + + public MatchResult CurResult; + + public struct MatchResult + { + private double _confidence = 0; // 匹配置信度 + public BaseMapLayerByTemplateMatch? Layer = null; // 地图信息 + public Point2f MapPos = new Point2f(0, 0); // 匹配位置 + public double Confidence + { + get => _confidence; + set + { + IsFailed = value < LowThreshold || value > 1.0; + IsSuccess = value >= HighThreshold && value <= 1.0; + _confidence = value; + } + } + public bool IsSuccess; + public bool IsFailed; + public MatchResult() {} + } + + protected SceneBaseMapByTemplateMatch( + MapTypes type, + Size mapSize, + Point2f mapOriginInImageCoordinate, + int mapImageBlockWidth, + int splitRow, + int splitCol) + : base(type, mapSize, mapOriginInImageCoordinate, mapImageBlockWidth, splitRow, splitCol) + { + } + + public override Point2f GetMiniMapPosition(Mat colorMiniMapMat) + { + var (miniMap, mask) = _miniMapPreprocessor.GetMiniMapAndMask(colorMiniMapMat); + using (miniMap) + using (mask) + { + GlobalMatch(miniMap, mask); + return CurResult.IsSuccess ? CurResult.MapPos : default; + } + } + + public override Point2f GetMiniMapPosition(Mat colorMiniMapMat, float prevX, float prevY) + { + if (prevX <= 0 || prevY <= 0) + { + return GetMiniMapPosition(colorMiniMapMat); + } + var (miniMap, mask) = _miniMapPreprocessor.GetMiniMapAndMask(colorMiniMapMat); + using (miniMap) + using (mask) + { + LocalMatch(miniMap, mask, new Point2f(prevX, prevY)); + return CurResult.IsSuccess ? CurResult.MapPos : default; + } + } + +/* + public SceneBaseMapByTemplateMatch FromJsonFiles(string filePath) + { + string json = File.ReadAllText(filePath); + var sceneBaseMap = JsonSerializer.Deserialize(json) ?? throw new Exception("Failed to deserialize JSON."); + sceneBaseMap.Type = SceneBaseMapByTemplateMatch.Type; + return sceneBaseMap; + } +*/ + + + #region 模板匹配 + + public void GlobalMatch(Mat miniMap, Mat mask) + { + using var context = new MatchContext(miniMap, mask); + RoughMatchGlobal(context); + ExactMatch(context); + } + + // 局部匹配:在上一次匹配位置附近进行搜索 + public void LocalMatch(Mat miniMap, Mat mask, Point2f pos) + { + using var context = new MatchContext(miniMap, mask); + RoughMatchLocal(context, pos); + ExactMatch(context); + } + + public void RoughMatchGlobal(MatchContext context) + { + CurResult = default; + var flag = false; + foreach (var layer in Layers) + { + var (tempPos, tempVal) = layer.RoughMatch(context.MaskedMiniMapRoughs, context.MaskRoughF); + if (!context.NormalizerRough.Update(tempVal + context.TplSumSq)) continue; + CurResult.Layer = layer; + CurResult.MapPos = tempPos; + flag = true; + } + + if (flag) + { + CurResult.Confidence = context.NormalizerRough.Confidence(); + Debug.WriteLine($"粗匹配成功, 坐标 {CurResult.MapPos}, 置信度 {CurResult.Confidence}"); + } + } + + public void RoughMatchLocal(MatchContext context, Point2f pos) + { + if (!CurResult.MapPos.Equals(pos)) + { + CurResult.Layer = null; + CurResult.MapPos = pos; + } + CurResult.Confidence = 0; + if (CurResult.Layer != null) + { + var (tempPos, tempVal) = CurResult.Layer.RoughMatch(context.MaskedMiniMapRoughs, context.MaskRoughF, pos); + if (context.NormalizerRough.Update(tempVal + context.TplSumSq)) + { + CurResult.MapPos = tempPos; + CurResult.Confidence = context.NormalizerRough.Confidence(); + } + } + if (CurResult.IsSuccess) return; + + var flag = false; + foreach (var layer in (CurResult.Layer == null)? Layers : Layers.Where(layer => layer != CurResult.Layer)) + { + var (tempPos, tempVal) = layer.RoughMatch(context.MaskedMiniMapRoughs, context.MaskRoughF, pos); + if (!context.NormalizerRough.Update(tempVal + context.TplSumSq)) continue; + CurResult.Layer = layer; + CurResult.MapPos = tempPos; + flag = true; + } + if (flag) CurResult.Confidence = context.NormalizerRough.Confidence(); + + if (CurResult.IsSuccess) return; + + RoughMatchLocalChan(context, pos); + + if (CurResult.IsSuccess) return; + + RoughMatchGlobal(context); + } + + public void RoughMatchLocalChan(MatchContext context, Point2f pos) + { + CurResult = default; + var flag = false; + foreach (var layer in Layers) + { + var (tempPos, tempVal) = layer.RoughMatch(context.MaskedMiniMapRoughs, context.MaskRoughF, pos, context.Channels); + if (!context.NormalizerRoughChan.Update(tempVal + context.TplSumSqChan)) continue; + CurResult.Layer = layer; + CurResult.MapPos = tempPos; + flag = true; + } + if (flag) CurResult.Confidence = context.NormalizerRough.Confidence(); + } + + public void ExactMatch(MatchContext context) + { + if (CurResult.Layer == null) return; + if (CurResult.IsFailed) return; + var (tempPos, tempVal) = CurResult.Layer.ExactMatch(context.MiniMapExact, context.MaskExact, CurResult.MapPos); + if (context.NormalizerExact.Update(tempVal)) + { + CurResult.MapPos = tempPos; + CurResult.Confidence = context.NormalizerExact.Confidence(); + } + else + { + CurResult = default; + } + } + #endregion + +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Map/MiniMap/CameraOrientationCalculator.cs b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/CameraOrientationCalculator.cs new file mode 100644 index 00000000..0364bb74 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/CameraOrientationCalculator.cs @@ -0,0 +1,107 @@ +using System.Linq; +using OpenCvSharp; +using System; + +namespace BetterGenshinImpact.GameTask.Common.Map.MiniMap; + +using static MiniMapPreprocessorUtils; + +public class CameraOrientationCalculator : IDisposable +{ + private const int TplOutRad = 78; + private const int TplInnRad = 19; + private const int RLength = 60; + private const int ThetaLength = 180; + private const int FLength = 256; + private readonly Mat _rotationRemapDataX = new Mat(); + private readonly Mat _rotationRemapDataY = new Mat(); + private readonly Mat _alphaMask2Remap; + + public CameraOrientationCalculator() + { + float[] rArray = LinearSpaced(TplInnRad, TplOutRad, RLength); + float[] thetaArray = LinearSpaced(0, 360, ThetaLength, false); + + using (var r = Mat.FromPixelData(1, RLength, MatType.CV_32F, rArray)) + using (var theta = Mat.FromPixelData(ThetaLength, 1, MatType.CV_32F, thetaArray)) + using (var rMat = r.Repeat(ThetaLength, 1)) + using (var thetaMat = theta.Repeat(1, RLength)) + { + Cv2.PolarToCart(rMat, thetaMat, _rotationRemapDataX, _rotationRemapDataY, true); + } + Cv2.Add(_rotationRemapDataX, Size / 2, _rotationRemapDataX); + Cv2.Add(_rotationRemapDataY, Size / 2, _rotationRemapDataY); + + using (var alphaMask2Row = Mat.FromPixelData(1, RLength, MatType.CV_32F, rArray.Select(v => (float)(111.7 + 1.836 * v)).ToArray())) + { + _alphaMask2Remap = alphaMask2Row.Repeat(ThetaLength, 1); + } + } + + public (float, float) PredictRotation(Mat outMat) + { + using var remap = new Mat(); + using var hImg = new Mat(); + using var faImg = new Mat(); + using var fbImg = new Mat(); + using var histA = new Mat(); + using var histB = new Mat(); + using var result = Mat.Zeros(1, ThetaLength, MatType.CV_32FC1).ToMat(); + using var resultShift = new Mat(); + using var mask = new Mat(); + using var maskSum = new Mat(); + Cv2.Remap(outMat, remap, _rotationRemapDataX, _rotationRemapDataY, InterpolationFlags.Nearest); + Cv2.InRange(remap, new Scalar(0, 0, 0), new Scalar(1, 1, 1), mask); + Cv2.BitwiseNot(mask,mask); + Cv2.Reduce(mask, maskSum, ReduceDimension.Column, ReduceTypes.Sum, MatType.CV_32F); + Cv2.Transpose(maskSum, maskSum); + BgrToHue(remap, hImg, faImg); + faImg.CopyTo(fbImg); + ApplyMask(fbImg, _alphaMask2Remap, new Scalar(255.0f)); + int[] channels = [0, 1]; + int[] histSize = [ThetaLength, FLength]; + Rangef[] ranges = [new(0, 180), new(0, 256)]; + Cv2.CalcHist([hImg, faImg], channels, null, histA, 2, histSize, ranges); + Cv2.CalcHist([hImg, fbImg], channels, null, histB, 2, histSize, ranges); + unsafe + { + var hImgPtr = (float*)hImg.Data; + var faImgPtr = (float*)faImg.Data; + var fbImgPtr = (float*)fbImg.Data; + var histAPtr = (float*)histA.Data; + var histBPtr = (float*)histB.Data; + var resultPtr = (float*)result.Data; + var totalElements = ThetaLength * RLength; + for (var i = 0; i < totalElements; i++) + { + float h = hImgPtr[i]; + float fa = faImgPtr[i]; + float fb = fbImgPtr[i]; + if (h is < 0 or >= 180 || fa is < 0 or >= 256 || fb is < 0 or >= 256) continue; + float ha = histAPtr[(int)(h / 180 * ThetaLength) * FLength + (int)(fa / 256 * FLength)]; + float hb = histBPtr[(int)(h / 180 * ThetaLength) * FLength + (int)(fb / 256 * FLength)]; + resultPtr[i / RLength] += (ha > hb) ? 0.0f : + (Math.Abs(ha - hb) < 0.0001) ? 60.0f : + 255.0f; + } + } + Cv2.Divide(result, maskSum, result); + Cv2.Resize(result, result, new Size(result.Width * 2, 1), 0, 0, InterpolationFlags.Cubic); + var peakWidth = ThetaLength / 2 + 1; + RightShiftCv(result, resultShift, peakWidth); + var peakRegionSum = Cv2.Sum(resultShift[0, 1, 0, peakWidth]).Val0; + Cv2.Subtract(result, resultShift, resultShift); + Cv2.Integral(resultShift, result); + Cv2.MinMaxLoc(result, out _, out var maxVal, out _, out var maxLoc); + var degree = (float)(maxLoc.X - 1) / ThetaLength * 180 - 45; + var rotationConfidence = (float)(maxVal + peakRegionSum) / (peakWidth * RLength * 255); + return (degree, rotationConfidence); + } + + public void Dispose() + { + _rotationRemapDataX.Dispose(); + _rotationRemapDataY.Dispose(); + _alphaMask2Remap.Dispose(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MaskCalculator.cs b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MaskCalculator.cs new file mode 100644 index 00000000..16b85d73 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MaskCalculator.cs @@ -0,0 +1,152 @@ +using OpenCvSharp; +using System; +using System.Linq; + +namespace BetterGenshinImpact.GameTask.Common.Map.MiniMap; + +using static MiniMapPreprocessorUtils; + +public class MaskCalculator : IDisposable +{ + private readonly Mat _alphaMask1 = new Mat(); + private readonly Mat _alphaMask2 = new Mat(); + private readonly Mat _radius = new Mat(); + private readonly Mat _angle = new Mat(); + private readonly Mat _sectorMask = new Mat(Size, Size, MatType.CV_8UC3); + private readonly Mat _circleMask = Mat.Zeros(Size, Size, MatType.CV_8UC1); + private readonly Mat _kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(5, 5)); + private readonly Mat _outMatF = new Mat(Size, Size, MatType.CV_32FC3); + private readonly Mat _outMask = new Mat(Size, Size, MatType.CV_8UC1); + + public MaskCalculator() + { + float[] xArray = LinearSpaced(-Size / 2.0f, Size / 2.0f, Size, false); + int[] range = Enumerable.Range(0, 256).ToArray(); + float[] alphaParams1 = [18.632f, 20.157f, 24.093f, 34.617f, 38.566f, 41.94f, 47.654f, 51.087f, 58.561f, 63.925f, 67.759f, 71.77f, 75.214f]; + + using (var x = Mat.FromPixelData(1, Size, MatType.CV_32FC1, xArray)) + using (var xMat = x.Repeat(Size, 1)) + using (var yMat = xMat.T()) + { + Cv2.CartToPolar(xMat, yMat, _radius, _angle, true); + } + _radius.ConvertTo(_radius, MatType.CV_8UC1); + Cv2.Divide(_angle, 2, _angle); + _angle.ConvertTo(_angle, MatType.CV_8UC1); + + var lutData1 = range.Select(v => + { + var index = Array.BinarySearch(alphaParams1, v); + return (byte)Math.Min(229 + (index < 0 ? ~index : index), 255); + }).ToArray(); + var lutData2 = range.Select(v => (byte)Math.Min(111.7 + 1.836 * v, 255)).ToArray(); + using var lut1 = Mat.FromPixelData(1, 256, MatType.CV_8UC1, lutData1); + using var lut2 = Mat.FromPixelData(1, 256, MatType.CV_8UC1, lutData2); + Cv2.LUT(_radius, lut1, _alphaMask1); + Cv2.LUT(_radius, lut2, _alphaMask2); + Cv2.CvtColor(_alphaMask1, _alphaMask1, ColorConversionCodes.GRAY2BGR); + Cv2.CvtColor(_alphaMask2, _alphaMask2, ColorConversionCodes.GRAY2BGR); + + _circleMask.Circle(Size / 2, Size / 2, Size / 2, new Scalar(255), -1); + } + + public Mat Process1(Mat bgrMat) + { + bgrMat = bgrMat[new Rect((bgrMat.Width - Size) / 2, (bgrMat.Width - Size) / 2, Size, Size)]; + bgrMat.ConvertTo(_outMatF, MatType.CV_32FC3); + ApplyMask(_outMatF, _alphaMask1, new Scalar(0.0f, 0.0f, 0.0f)); + CreateIconMask(bgrMat); + var outMat = new Mat(); + _outMatF.ConvertTo(outMat, MatType.CV_8UC3); + outMat.SetTo(new Scalar(0, 0, 0), _outMask); + return outMat; + } + + public (Mat, Mat) Process2(float angle) + { + CreatSectorMask((int)Math.Round(angle)); + ApplyMask(_outMatF, _sectorMask, new Scalar(255.0f, 255.0f, 255.0f)); + var outMat = new Mat(); + _outMatF.ConvertTo(outMat, MatType.CV_8UC3); + Cv2.BitwiseNot(_outMask, _outMask); + CreatBgMask(outMat); + Cv2.BitwiseAnd(_circleMask, _outMask, _outMask); + return (outMat, _outMask.Clone()); + } + + private void CreatSectorMask(int angle) + { + _alphaMask2.CopyTo(_sectorMask); + Cv2.Ellipse(_sectorMask, new Point(Size / 2, Size / 2), new Size(Size, Size), 0, angle + 45.5, angle + 314.5, new Scalar(255, 255, 255), -1); + } + + private void CreatBgMask(Mat bgrMat) + { + using var temp = new Mat(); + Cv2.InRange(bgrMat, new Scalar(165, 165, 55), new Scalar(180, 180, 75), temp); + Cv2.MorphologyEx(temp, temp, MorphTypes.Open, Mat.Ones(2, 2, MatType.CV_8UC1)); + if (Cv2.CountNonZero(temp) == 0) + { + return; + } + var minDist = new byte[256]; + Array.Fill(minDist, (byte)255); + CalculateMinDist(temp, minDist); + using var lut = Mat.FromPixelData(1, 256, MatType.CV_8UC1, minDist); + Cv2.LUT(_angle, lut, temp); + Cv2.Compare(_radius, temp, temp, CmpType.LT); + using var temp1 = new Mat(); + Cv2.InRange(bgrMat, Scalar.All(100), Scalar.All(255), temp1); + Cv2.BitwiseOr(temp1, temp, _outMask, _outMask); + } + + private void CreateIconMask(Mat bgrMat) + { + using var cmax = new Mat(); + using var cmin = new Mat(); + using var diff = new Mat(); + MaxMinChannels(bgrMat, cmax, cmin); + Cv2.Compare(cmax, cmin, _outMask, CmpType.EQ); + Cv2.Subtract(cmax, cmin, diff); + Cv2.Subtract(255, cmax, cmin); + Cv2.Divide(cmin, 6, cmin); + Cv2.Min(cmin, diff, diff); + Cv2.Add(diff, 10, diff); + cmax.SetTo(255, _outMask); + Cv2.Divide(cmax, diff, cmax, 10); + Cv2.Threshold(cmax, _outMask, 200, 255, ThresholdTypes.Binary); + Cv2.Dilate(_outMask, _outMask, _kernel); + Cv2.MorphologyEx(_outMask, _outMask, MorphTypes.Close, _kernel); + } + + private unsafe void CalculateMinDist(Mat mask, byte[] minDist) + { + var maskPtr = (byte*)mask.Data; + var anglePtr = (byte*)_angle.Data; + var radiusPtr = (byte*)_radius.Data; + long totalBytes = Size * Size; + for (var i = 0; i < totalBytes; i++) + { + if (maskPtr[i] != 255) continue; + byte a = anglePtr[i]; + byte r = radiusPtr[i]; + if (minDist[a] > r) + { + minDist[a] = r; + } + } + } + + public void Dispose() + { + _alphaMask1.Dispose(); + _alphaMask2.Dispose(); + _radius.Dispose(); + _angle.Dispose(); + _sectorMask.Dispose(); + _circleMask.Dispose(); + _kernel.Dispose(); + _outMatF.Dispose(); + _outMask.Dispose(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MiniMapPreprocessor.cs b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MiniMapPreprocessor.cs new file mode 100644 index 00000000..69f94cb8 --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MiniMapPreprocessor.cs @@ -0,0 +1,36 @@ +using System; +using System.Diagnostics; +using OpenCvSharp; + +namespace BetterGenshinImpact.GameTask.Common.Map.MiniMap; + +public class MiniMapPreprocessor : IDisposable +{ + private static readonly MaskCalculator _maskCalculator = new(); + private static readonly CameraOrientationCalculator _coCalculator = new(); + + public (float, float) PredictRotationWithConfidence(Mat miniMap) + { + using var mat = _maskCalculator.Process1(miniMap); + return _coCalculator.PredictRotation(mat); + } + + public float PredictRotation(Mat miniMap) + { + return PredictRotationWithConfidence(miniMap).Item1; + } + + public (Mat, Mat) GetMiniMapAndMask(Mat miniMap) + { + //Debug.WriteLine($"输入图片尺寸为{miniMap.Size()} 类型为 {miniMap.Type()}"); + using var mat = _maskCalculator.Process1(miniMap); + var (angle, _) = _coCalculator.PredictRotation(mat); + return _maskCalculator.Process2(angle); + } + + public void Dispose() + { + _coCalculator.Dispose(); + _maskCalculator.Dispose(); + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MiniMapPreprocessorUtils.cs b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MiniMapPreprocessorUtils.cs new file mode 100644 index 00000000..4c8a15ce --- /dev/null +++ b/BetterGenshinImpact/GameTask/Common/Map/MiniMap/MiniMapPreprocessorUtils.cs @@ -0,0 +1,58 @@ +using System.Linq; +using OpenCvSharp; + +namespace BetterGenshinImpact.GameTask.Common.Map.MiniMap; + +public static class MiniMapPreprocessorUtils +{ + public const int Size = 156; + + public static float[] LinearSpaced(float a, float b, int n, bool endpoint = true) + { + return endpoint ? + Enumerable.Range(0, n).Select(i => a + (b - a) * i / (n - 1)).ToArray() : + Enumerable.Range(0, n).Select(i => a + (b - a) * i / n).ToArray(); + } + + public static void MaxMinChannels(Mat bgrMat, Mat cmax, Mat cmin) + { + var bgr = bgrMat.Split(); + using (bgr[0]) + using (bgr[1]) + using (bgr[2]) + { + Cv2.Max(bgr[2], bgr[1], cmax); + Cv2.Max(cmax, bgr[0], cmax); + Cv2.Min(bgr[2], bgr[1], cmin); + Cv2.Min(cmin, bgr[0], cmin); + } + } + + public static void ApplyMask(Mat inputMat, Mat mask, Scalar bkg) + { + Cv2.Subtract(inputMat, bkg, inputMat); + Cv2.Divide(inputMat, mask, inputMat, 255, MatType.CV_32F); + Cv2.Add(inputMat, bkg, inputMat); + } + + public static void BgrToHue(Mat bgrMat, Mat hImg, Mat faImg) + { + using var hls = new Mat(); + using var h = new Mat(); + using var fa = new Mat(); + Cv2.CvtColor(bgrMat, hls, ColorConversionCodes.BGR2HLS_FULL); + Cv2.CvtColor(bgrMat, fa, ColorConversionCodes.BGR2GRAY); + Cv2.ExtractChannel(hls, h, 0); + h.ConvertTo(hImg, MatType.CV_32FC1); + fa.ConvertTo(faImg, MatType.CV_32FC1); + } + + public static void RightShiftCv(Mat input, Mat output, int k) + { + var part1 = input[0, input.Rows, input.Cols - k, input.Cols]; // 后k个元素 + var part2 = input[0, input.Rows, 0, input.Cols - k]; // 前面的元素 + Cv2.HConcat(part1, part2, output); + } + + +} \ No newline at end of file diff --git a/BetterGenshinImpact/GameTask/Common/NewRetry.cs b/BetterGenshinImpact/GameTask/Common/NewRetry.cs index d338e110..21c0910d 100644 --- a/BetterGenshinImpact/GameTask/Common/NewRetry.cs +++ b/BetterGenshinImpact/GameTask/Common/NewRetry.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using BetterGenshinImpact.Core.Recognition; +using static BetterGenshinImpact.GameTask.Common.TaskControl; namespace BetterGenshinImpact.GameTask.Common; @@ -12,6 +14,12 @@ namespace BetterGenshinImpact.GameTask.Common; /// public static class NewRetry { + /// + /// 重试指定操作,若抛出 RetryException 则在指定间隔后重试,最多尝试 maxAttemptCount 次。 + /// + /// 要执行的操作 + /// 重试间隔 + /// 最大尝试次数 public static void Do(Action action, TimeSpan retryInterval, int maxAttemptCount = 3) { _ = Do(() => @@ -21,6 +29,14 @@ public static class NewRetry }, retryInterval, maxAttemptCount); } + /// + /// 重试指定操作,若抛出 RetryException 则在指定间隔后重试,最多尝试 maxAttemptCount 次,返回操作结果。 + /// + /// 返回值类型 + /// 要执行的操作 + /// 重试间隔 + /// 最大尝试次数 + /// 操作结果 public static T Do(Func action, TimeSpan retryInterval, int maxAttemptCount = 3) { List exceptions = []; @@ -49,6 +65,14 @@ public static class NewRetry throw new AggregateException(exceptions); } + /// + /// 重试执行 action,直到返回 true 或达到最大重试次数。 + /// + /// 判断条件 + /// 取消令牌 + /// 最大重试次数 + /// 每次重试间隔(毫秒) + /// 是否成功 public static async Task WaitForAction(Func action, CancellationToken ct, int retryTimes = 10, int delayMs = 1000) { for (var i = 0; i < retryTimes; i++) @@ -61,4 +85,84 @@ public static class NewRetry } return false; } + + /// + /// 重试直到某个元素出现,可执行键盘或鼠标操作。 + /// + /// 要识别的目标对象 + /// 每次重试时执行的操作 + /// 取消令牌 + /// 最大尝试次数 + /// 重试间隔(毫秒) + /// 是否成功找到元素 + public static async Task WaitForElementAppear( + RecognitionObject recognitionObject, + Action retryAction, + CancellationToken ct, + int maxAttemptCount = 10, + int retryInterval = 1000 + ) + { + for (int i = 0; i < maxAttemptCount; i++) + { + if (ct.IsCancellationRequested) return false; + + // 执行重试操作(如按键) + retryAction?.Invoke(); + + // 等待指定时间 + await TaskControl.Delay(retryInterval, ct); + + // 截图并查找元素 + using var screen = CaptureToRectArea(); + using var result = screen.Find(recognitionObject); + + // 元素已出现 + if (!result.IsEmpty()) + { + return true; + } + } + return false; + } + + /// + /// 重试直到某个元素消失,可执行键盘或鼠标操作。 + /// + /// 要识别的目标对象 + /// 每次重试时执行的操作 + /// 取消令牌 + /// 最大尝试次数 + /// 重试间隔(毫秒) + /// 是否成功等待元素消失 + public static async Task WaitForElementDisappear( + RecognitionObject recognitionObject, + Action retryAction, + CancellationToken ct, + int maxAttemptCount = 10, + int retryInterval = 1000) + { + for (int i = 0; i < maxAttemptCount; i++) + { + if (ct.IsCancellationRequested) return false; + + // 执行重试操作(如按键) + retryAction?.Invoke(); + + // 等待指定时间 + await TaskControl.Delay(retryInterval, ct); + + // 截图并查找元素 + using var screen = CaptureToRectArea(); + using var result = screen.Find(recognitionObject); + + // 元素已消失 + if (result.IsEmpty()) + { + return true; + } + } + return false; + } } + diff --git a/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs new file mode 100644 index 00000000..5b24d52f --- /dev/null +++ b/BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs @@ -0,0 +1,232 @@ +using BetterGenshinImpact.Core.Simulator; +using BetterGenshinImpact.GameTask.Common; +using BetterGenshinImpact.GameTask.Model.Area; +using Fischless.WindowsInput; +using Microsoft.Extensions.Logging; +using OpenCvSharp; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace GameTask.Model.GameUI +{ + public class GridScreen : IAsyncEnumerable + { + private readonly Rect gridRoi; + private readonly CancellationToken ct; + private readonly ILogger logger; + private readonly InputSimulator input = Simulation.SendInput; + private readonly int s1Round; + private readonly int roundMilliseconds; + private readonly int s2Round; + private readonly double s3Scale; + + /// + /// 对Gird类型界面的操作封装类 + /// 直接对此类对象进行遍历即可获取所有项 + /// 每次的截图是上次滚动后的,如果实时性要求高,应每次迭代自行截图 + /// 在末页可能重复返回GridItem,须自行处理 + /// + /// Grid所在位置 + /// + /// + public GridScreen(Rect gridRoi, int s1Round, int roundMilliseconds, int s2Round, double s3Scale, ILogger logger, CancellationToken ct) + { + this.gridRoi = gridRoi; + this.ct = ct; + this.logger = logger; + this.s1Round = s1Round; + this.roundMilliseconds = roundMilliseconds; + this.s2Round = s2Round; + this.s3Scale = s3Scale; + } + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new GridEnumerator(this.gridRoi, this.s1Round, this.roundMilliseconds, this.s2Round, this.s3Scale, this.logger, this.input, this.ct); + } + + public class GridEnumerator : IAsyncEnumerator + { + private readonly Rect roi; + private readonly CancellationToken ct; + private readonly ILogger logger; + private readonly InputSimulator input = Simulation.SendInput; + private readonly int s1Round; + private readonly int roundMilliseconds; + private readonly int s2Round; + private readonly double s3Scale; + + private record Page(ImageRegion ImageRegion, Stack Rects); + private Page? currentPage; + private ImageRegion current; + ImageRegion IAsyncEnumerator.Current => current; + + /// + /// 滚动操作枚举器 + /// + /// + /// 测试是否能滚动时发出的滚动命令次数 + /// 滚动命令间隔毫秒 + /// 滚过一整页时发出的滚动命令次数 + /// 微调滚动时控制首行距离上边界的参数 + /// + /// + /// + public GridEnumerator(Rect roi, int s1Round, int roundMilliseconds, int s2Round, double s3Scale, ILogger logger, InputSimulator input, CancellationToken ct) + { + this.roi = roi; + this.ct = ct; + this.logger = logger; + this.input = input; + this.s1Round = s1Round; + this.roundMilliseconds = roundMilliseconds; + this.s2Round = s2Round; + this.s3Scale = s3Scale; + } + + public async Task TryVerticalScollDown() + { + using var ra = TaskControl.CaptureToRectArea(); + using ImageRegion prevGrid = ra.DeriveCrop(roi); + + for (int i = 0; i < this.s1Round; i++) + { + this.input.Mouse.VerticalScroll(-2); + await TaskControl.Delay(this.roundMilliseconds, this.ct); + } + await TaskControl.Delay(300, this.ct); + using var ra2 = TaskControl.CaptureToRectArea(); + using ImageRegion scrolledGrid = ra2.DeriveCrop(this.roi); + + bool isScrolling = IsScrolling(prevGrid.CacheGreyMat, scrolledGrid.CacheGreyMat, out Point2d shift, logger: this.logger); + + return isScrolling; + } + + /// + /// 判断是否还能继续滚动,如果到底了则只能滚动一丝并很快地回弹 + /// + /// 先前的灰度图 + /// 尝试滚动并等待可能的回弹后的灰度图 + /// 估计的位移 + /// 低于下限则可能不存在平移 + /// 上限用于抵消微小的其他差异 + /// + /// + public static bool IsScrolling(Mat prevGray, Mat nextGray, out Point2d shift, double lowerThreshold = 0.5, double upperThreshold = 0.95, ILogger? logger = null) + { + using Mat prev = new Mat(); + prevGray.ConvertTo(prev, MatType.CV_32FC1); + using Mat next = new Mat(); + nextGray.ConvertTo(next, MatType.CV_32FC1); + + using Mat window = new Mat(); + shift = Cv2.PhaseCorrelate(prev, next, window, out double response); // 相位相关性 + logger?.LogInformation($"response={response:F3}, shift=({shift.X:F2}, {shift.Y:F2})"); + return response > lowerThreshold && response < upperThreshold; + } + + public static IEnumerable GetGridItems(Mat src) + { + using Mat grey = src.CvtColor(ColorConversionCodes.BGR2GRAY); + + using Mat canny = grey.Canny(20, 40); + + Cv2.FindContours(canny, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null); + + IEnumerable boxes = contours.Where(c => Cv2.MinAreaRect(c).Angle % 90 <= 1) // 剔除倾斜 + .Select(Cv2.BoundingRect).Where(r => + { + if (r.Height == 0) + { + return false; + } + return Math.Abs((float)r.Width / r.Height - 0.8) < 0.05; // 按形状筛选 + }).ToList(); + + //src.DrawContours(contours, -1, Scalar.Red); + + int biggestRectHeight = boxes.Max(b => b.Height); + boxes = boxes.Where(b => (float)b.Height / biggestRectHeight > 0.88); // 剔除太小的 + + return boxes.ToArray(); + } + + public async ValueTask MoveNextAsync() + { + if (this.currentPage == null || this.currentPage.Rects.Count < 1) + { + if (this.currentPage != null) + { + //BetterGenshinImpact.View.Drawable.VisionContext.Instance().DrawContent.ClearAll(); + + using var ra4 = TaskControl.CaptureToRectArea(); + ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2); + await TaskControl.Delay(300, ct); + + bool canScoll = await TryVerticalScollDown(); + + if (canScoll) + { + for (int i = 0; i < this.s2Round; i++) // 再滚动差不多(最多行数-1)行 + { + input.Mouse.VerticalScroll(-2); + await TaskControl.Delay(this.roundMilliseconds, ct); + } + + DateTimeOffset rollingEndTime = DateTime.Now.AddSeconds(2); + while (DateTime.Now < rollingEndTime) + { + await TaskControl.Delay(60, ct); + using var ra2 = TaskControl.CaptureToRectArea(); + using ImageRegion grid2 = ra2.DeriveCrop(this.roi); + IEnumerable gridItems2 = GetGridItems(grid2.SrcMat); + if (gridItems2.Min(i => i.Y) > (ra2.Width * this.s3Scale)) // 最后精细滚动,保证完整地显示最多行 + { + input.Mouse.VerticalScroll(-1); + } + else + { + break; + } + } + using var ra3 = TaskControl.CaptureToRectArea(); + using ImageRegion grid3 = ra3.DeriveCrop(this.roi); + grid3.MoveTo(grid3.Width, grid3.Height); + await TaskControl.Delay(300, ct); + } + else + { + await TaskControl.Delay(300, ct); + this.logger.LogInformation("滚动到底部了"); + return false; + } + } + + using var ra = TaskControl.CaptureToRectArea(); + var imageRegion = ra.DeriveCrop(this.roi); + IEnumerable gridItems = GetGridItems(imageRegion.SrcMat); + this.currentPage = new Page(imageRegion, new Stack(gridItems)); + + //foreach (Rect item in gridItems) + //{ + // imageRegion.DrawRect(item, item.GetHashCode().ToString(), new System.Drawing.Pen(System.Drawing.Color.Blue)); + //} + } + + this.current = this.currentPage.ImageRegion.DeriveCrop(this.currentPage.Rects.Pop()); + return true; + } + + public ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } + } + } +} diff --git a/BetterGenshinImpact/Helpers/StringUtils.cs b/BetterGenshinImpact/Helpers/StringUtils.cs index cf31186d..5a4577b9 100644 --- a/BetterGenshinImpact/Helpers/StringUtils.cs +++ b/BetterGenshinImpact/Helpers/StringUtils.cs @@ -88,21 +88,21 @@ public partial class StringUtils return int.TryParse(text, out int result) ? result : defaultValue; } - public static int TryExtractPositiveInt(string text) + public static int TryExtractPositiveInt(string text, int defaultValue = -1) { if (string.IsNullOrEmpty(text)) { - return -1; + return defaultValue; } try { text = RegexHelper.ExcludeNumberRegex().Replace(text, ""); - return int.Parse(text); + return TryParseInt(text, defaultValue); } catch { - return -1; + return defaultValue; } } diff --git a/BetterGenshinImpact/Helpers/Win32/MirrorChyanHelper.cs b/BetterGenshinImpact/Helpers/Win32/MirrorChyanHelper.cs index 0aff18d7..1f22f189 100644 --- a/BetterGenshinImpact/Helpers/Win32/MirrorChyanHelper.cs +++ b/BetterGenshinImpact/Helpers/Win32/MirrorChyanHelper.cs @@ -1,5 +1,6 @@ using System; using Windows.System; +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.View.Windows; using Meziantou.Framework.Win32; using Wpf.Ui.Violeta.Controls; @@ -31,7 +32,7 @@ public static class MirrorChyanHelper LeftButtonText = "获取CDK", LeftButtonClick = (sender, args) => { - Launcher.LaunchUriAsync(new Uri("https://mirrorchyan.com/zh/get-start")); + OpenMirrorChyanWebsite(); } } ); @@ -67,7 +68,7 @@ public static class MirrorChyanHelper LeftButtonText = "获取CDK", LeftButtonClick = (sender, args) => { - Launcher.LaunchUriAsync(new Uri("https://mirrorchyan.com/zh/get-start")); + OpenMirrorChyanWebsite(); } } ); @@ -90,4 +91,10 @@ public static class MirrorChyanHelper { CredentialManagerHelper.DeleteCredential(MirrorChyanCdkAppName); } + + + private static void OpenMirrorChyanWebsite() + { + Launcher.LaunchUriAsync(new Uri($"https://mirrorchyan.com/zh/get-start?source=bgi-{Global.Version}")); + } } \ No newline at end of file diff --git a/BetterGenshinImpact/View/Controls/Drawer/CustomDrawer.cs b/BetterGenshinImpact/View/Controls/Drawer/CustomDrawer.cs new file mode 100644 index 00000000..c3e8fac4 --- /dev/null +++ b/BetterGenshinImpact/View/Controls/Drawer/CustomDrawer.cs @@ -0,0 +1,410 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.ComponentModel; + +namespace BetterGenshinImpact.View.Controls.Drawer; + +public class CustomDrawer : ContentControl +{ + #region 依赖属性 + + public static readonly DependencyProperty IsOpenProperty = + DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(CustomDrawer), + new PropertyMetadata(false, OnIsOpenChanged)); + + public static readonly DependencyProperty DrawerPositionProperty = + DependencyProperty.Register(nameof(DrawerPosition), typeof(DrawerPosition), typeof(CustomDrawer), + new PropertyMetadata(DrawerPosition.Right, OnDrawerPositionChanged)); + + public static readonly DependencyProperty OpenWidthProperty = + DependencyProperty.Register(nameof(OpenWidth), typeof(double), typeof(CustomDrawer), + new PropertyMetadata(400.0)); + + public static readonly DependencyProperty OpenHeightProperty = + DependencyProperty.Register(nameof(OpenHeight), typeof(double), typeof(CustomDrawer), + new PropertyMetadata(300.0)); + + public static readonly DependencyProperty AnimationDurationProperty = + DependencyProperty.Register(nameof(AnimationDuration), typeof(TimeSpan), typeof(CustomDrawer), + new PropertyMetadata(TimeSpan.FromMilliseconds(200))); + + public static readonly DependencyProperty BackgroundOpacityProperty = + DependencyProperty.Register(nameof(BackgroundOpacity), typeof(double), typeof(CustomDrawer), + new PropertyMetadata(0.6)); + + public static readonly DependencyProperty DrawerBackgroundProperty = + DependencyProperty.Register(nameof(DrawerBackground), typeof(Brush), typeof(CustomDrawer), + new PropertyMetadata(Brushes.Black)); + + #endregion + + #region 事件 + + /// + /// 抽屉打开后触发的事件 + /// + public event EventHandler Opened; + + /// + /// 抽屉关闭前触发的事件,可以取消关闭操作 + /// + public event CancelEventHandler Closing; + + /// + /// 引发 Opened 事件 + /// + protected virtual void OnOpened() + { + Opened?.Invoke(this, EventArgs.Empty); + } + + /// + /// 引发 Closing 事件 + /// + /// 如果取消关闭,则返回 true;否则返回 false + protected virtual bool OnClosing() + { + if (Closing != null) + { + CancelEventArgs args = new CancelEventArgs(); + Closing(this, args); + return args.Cancel; + } + return false; + } + + #endregion + + #region 属性 + + public bool IsOpen + { + get => (bool)GetValue(IsOpenProperty); + set => SetValue(IsOpenProperty, value); + } + + public DrawerPosition DrawerPosition + { + get => (DrawerPosition)GetValue(DrawerPositionProperty); + set => SetValue(DrawerPositionProperty, value); + } + + public double OpenWidth + { + get => (double)GetValue(OpenWidthProperty); + set => SetValue(OpenWidthProperty, value); + } + + public double OpenHeight + { + get => (double)GetValue(OpenHeightProperty); + set => SetValue(OpenHeightProperty, value); + } + + public TimeSpan AnimationDuration + { + get => (TimeSpan)GetValue(AnimationDurationProperty); + set => SetValue(AnimationDurationProperty, value); + } + + public double BackgroundOpacity + { + get => (double)GetValue(BackgroundOpacityProperty); + set => SetValue(BackgroundOpacityProperty, value); + } + + public Brush DrawerBackground + { + get => (Brush)GetValue(DrawerBackgroundProperty); + set => SetValue(DrawerBackgroundProperty, value); + } + + #endregion + + private Border _backgroundOverlay; + private Border _drawerContainer; + private Grid _mainGrid; + + static CustomDrawer() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomDrawer), + new FrameworkPropertyMetadata(typeof(CustomDrawer))); + } + + public CustomDrawer() + { + this.Loaded += CustomDrawer_Loaded; + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _mainGrid = GetTemplateChild("PART_MainGrid") as Grid; + _backgroundOverlay = GetTemplateChild("PART_BackgroundOverlay") as Border; + _drawerContainer = GetTemplateChild("PART_DrawerContainer") as Border; + + if (_backgroundOverlay != null) + { + _backgroundOverlay.MouseDown += BackgroundOverlay_MouseDown; + } + + UpdateDrawerPosition(); + UpdateOpenState(false); + } + + private void CustomDrawer_Loaded(object sender, RoutedEventArgs e) + { + UpdateOpenState(false); + } + + private void BackgroundOverlay_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + CloseDrawer(); + } + + private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is CustomDrawer drawer) + { + bool newValue = (bool)e.NewValue; + bool oldValue = (bool)e.OldValue; + + // 如果是从打开到关闭状态,需要触发关闭前事件 + if (oldValue && !newValue) + { + bool cancel = drawer.OnClosing(); + if (cancel) + { + // 如果取消关闭,则恢复 IsOpen 为 true + drawer.IsOpen = true; + return; + } + } + + // 更新抽屉状态(动画) + drawer.UpdateOpenState(true); + + } + } + + private static void OnDrawerPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is CustomDrawer drawer) + { + drawer.UpdateDrawerPosition(); + drawer.UpdateOpenState(false); + } + } + + private void UpdateDrawerPosition() + { + if (_drawerContainer == null) return; + + switch (DrawerPosition) + { + case DrawerPosition.Left: + _drawerContainer.HorizontalAlignment = HorizontalAlignment.Left; + _drawerContainer.VerticalAlignment = VerticalAlignment.Stretch; + _drawerContainer.Width = OpenWidth; + _drawerContainer.Height = double.NaN; + break; + case DrawerPosition.Right: + _drawerContainer.HorizontalAlignment = HorizontalAlignment.Right; + _drawerContainer.VerticalAlignment = VerticalAlignment.Stretch; + _drawerContainer.Width = OpenWidth; + _drawerContainer.Height = double.NaN; + break; + case DrawerPosition.Top: + _drawerContainer.HorizontalAlignment = HorizontalAlignment.Stretch; + _drawerContainer.VerticalAlignment = VerticalAlignment.Top; + _drawerContainer.Width = double.NaN; + _drawerContainer.Height = OpenHeight; + break; + case DrawerPosition.Bottom: + _drawerContainer.HorizontalAlignment = HorizontalAlignment.Stretch; + _drawerContainer.VerticalAlignment = VerticalAlignment.Bottom; + _drawerContainer.Width = double.NaN; + _drawerContainer.Height = OpenHeight; + break; + } + } + + private void UpdateOpenState(bool animate) + { + if (_drawerContainer == null || _backgroundOverlay == null) return; + + _backgroundOverlay.IsHitTestVisible = IsOpen; + + _drawerContainer.Opacity = 1; + + if (IsOpen) + { + Visibility = Visibility.Visible; + } + + // 每次更新状态时重新应用宽高 + switch (DrawerPosition) + { + case DrawerPosition.Left: + case DrawerPosition.Right: + _drawerContainer.Width = OpenWidth; + _drawerContainer.Height = double.NaN; + break; + case DrawerPosition.Top: + case DrawerPosition.Bottom: + _drawerContainer.Width = double.NaN; + _drawerContainer.Height = OpenHeight; + break; + } + + if (animate) + { + // 动画背景遮罩 + DoubleAnimation backgroundAnimation = new DoubleAnimation + { + To = IsOpen ? BackgroundOpacity : 0, + Duration = AnimationDuration + }; + _backgroundOverlay.BeginAnimation(OpacityProperty, backgroundAnimation); + + // 确保RenderTransform已设置 + if (_drawerContainer.RenderTransform == null || !(_drawerContainer.RenderTransform is TranslateTransform)) + { + _drawerContainer.RenderTransform = new TranslateTransform(); + } + + TranslateTransform transform = (TranslateTransform)_drawerContainer.RenderTransform; + + // 动画抽屉 + DoubleAnimation drawerAnimation = new DoubleAnimation + { + Duration = AnimationDuration, + // 弹性动画效果 + // EasingFunction = IsOpen + // ? new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.3 } + // : new ExponentialEase { EasingMode = EasingMode.EaseIn, Exponent = 6 } + }; + + // 如果是打开操作,在动画完成时触发Opened事件 + if (IsOpen) + { + drawerAnimation.Completed += (s, e) => OnOpened(); + } + + switch (DrawerPosition) + { + case DrawerPosition.Left: + // 打开时,先设置初始位置 + if (IsOpen) + { + transform.X = -OpenWidth; + } + + drawerAnimation.To = IsOpen ? 0 : -OpenWidth; + transform.BeginAnimation(TranslateTransform.XProperty, drawerAnimation); + break; + case DrawerPosition.Right: + // 打开时,先设置初始位置 + if (IsOpen) + { + transform.X = OpenWidth; + } + + drawerAnimation.To = IsOpen ? 0 : OpenWidth; + transform.BeginAnimation(TranslateTransform.XProperty, drawerAnimation); + break; + case DrawerPosition.Top: + // 打开时,先设置初始位置 + if (IsOpen) + { + transform.Y = -OpenHeight; + } + + drawerAnimation.To = IsOpen ? 0 : -OpenHeight; + transform.BeginAnimation(TranslateTransform.YProperty, drawerAnimation); + break; + case DrawerPosition.Bottom: + // 打开时,先设置初始位置 + if (IsOpen) + { + transform.Y = OpenHeight; + } + + drawerAnimation.To = IsOpen ? 0 : OpenHeight; + transform.BeginAnimation(TranslateTransform.YProperty, drawerAnimation); + break; + } + + if (!IsOpen) + { + drawerAnimation.Completed += (s, e) => + { + if (!IsOpen) + { + Visibility = Visibility.Collapsed; + } + }; + } + } + else + { + // 无动画直接设置 + _backgroundOverlay.Opacity = IsOpen ? BackgroundOpacity : 0; + + TranslateTransform transform = new TranslateTransform(); + _drawerContainer.RenderTransform = transform; + + switch (DrawerPosition) + { + case DrawerPosition.Left: + transform.X = IsOpen ? 0 : -OpenWidth; + break; + case DrawerPosition.Right: + transform.X = IsOpen ? 0 : OpenWidth; + break; + case DrawerPosition.Top: + transform.Y = IsOpen ? 0 : -OpenHeight; + break; + case DrawerPosition.Bottom: + transform.Y = IsOpen ? 0 : OpenHeight; + break; + } + + Visibility = IsOpen ? Visibility.Visible : Visibility.Collapsed; + + // 如果是无动画模式且正在打开,立即触发Opened事件 + if (IsOpen) + { + OnOpened(); + } + } + } + + /// + /// 关闭抽屉的方法,会触发 Closing 事件并可能被取消 + /// + /// 如果成功关闭返回 true,如果被取消返回 false + public bool CloseDrawer() + { + if (!IsOpen) + return true; + + if (OnClosing()) + return false; + + IsOpen = false; + return true; + } +} + +public enum DrawerPosition +{ + Left, + Right, + Top, + Bottom +} \ No newline at end of file diff --git a/BetterGenshinImpact/View/Controls/Drawer/DrawerStyles.xaml b/BetterGenshinImpact/View/Controls/Drawer/DrawerStyles.xaml new file mode 100644 index 00000000..947b7809 --- /dev/null +++ b/BetterGenshinImpact/View/Controls/Drawer/DrawerStyles.xaml @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Controls/Drawer/DrawerViewModel.cs b/BetterGenshinImpact/View/Controls/Drawer/DrawerViewModel.cs new file mode 100644 index 00000000..820e7bf8 --- /dev/null +++ b/BetterGenshinImpact/View/Controls/Drawer/DrawerViewModel.cs @@ -0,0 +1,71 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System; +using System.ComponentModel; +using System.Windows.Input; + +namespace BetterGenshinImpact.View.Controls.Drawer; + +public partial class DrawerViewModel : ObservableObject +{ + [ObservableProperty] + private bool _isDrawerOpen; + + [ObservableProperty] + private object? _drawerContent; + + [ObservableProperty] + private DrawerPosition _drawerPosition = DrawerPosition.Right; + + [ObservableProperty] + private double _drawerWidth = 400; + + [ObservableProperty] + private double _drawerHeight = 300; + + [ObservableProperty] + private RelayCommand _onDrawerOpenedCommand; + + [ObservableProperty] + private RelayCommand _onDrawerClosingCommand; + + public void setDrawerOpenedAction(Action action) + { + OnDrawerOpenedCommand = new RelayCommand(action!); + } + + public void SetDrawerClosingAction(Action action) + { + OnDrawerClosingCommand = new RelayCommand(action!); + } + + [RelayCommand] + public void OpenDrawer(object content) + { + DrawerContent = content; + IsDrawerOpen = true; + } + + [RelayCommand] + public void CloseDrawer() + { + IsDrawerOpen = false; + } + + [RelayCommand] + public void ToggleDrawer(object? content = null) + { + if (IsDrawerOpen) + { + CloseDrawer(); + } + else + { + if (content != null) + { + DrawerContent = content; + } + IsDrawerOpen = true; + } + } +} diff --git a/BetterGenshinImpact/View/Controls/Webview/WebpagePanel.cs b/BetterGenshinImpact/View/Controls/Webview/WebpagePanel.cs index dfb50fcf..394e5556 100644 --- a/BetterGenshinImpact/View/Controls/Webview/WebpagePanel.cs +++ b/BetterGenshinImpact/View/Controls/Webview/WebpagePanel.cs @@ -4,11 +4,13 @@ using Microsoft.Web.WebView2.Wpf; using System; using System.Diagnostics; using System.IO; +using System.Net; using System.Security.AccessControl; using System.Text; using System.Threading; using System.Windows; using System.Windows.Controls; +using BetterGenshinImpact.Helpers; namespace BetterGenshinImpact.View.Controls.Webview; @@ -23,6 +25,8 @@ public class WebpagePanel : UserControl public string? DownloadFolderPath { get; set; } public Action? OnWebViewInitializedAction { get; set; } + + public Action? OnNavigationCompletedAction { get; set; } public WebpagePanel() { @@ -45,10 +49,37 @@ public class WebpagePanel : UserControl }; _webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted; _webView.NavigationStarting += NavigationStarting_CancelNavigation; + _webView.NavigationCompleted += WebView_NavigationCompleted; + Content = _webView; } } - + + // public WebpagePanel(WebView2 webView2) + // { + // if (!IsWebView2Available()) + // { + // Content = CreateDownloadButton(); + // } + // else + // { + // EnsureWebView2DataFolder(); + // _webView = webView2; + // webView2.CreationProperties = new CoreWebView2CreationProperties + // { + // UserDataFolder = Path.Combine(new FileInfo(Environment.ProcessPath!).DirectoryName!, @"WebView2Data\\"), + // }; + // webView2.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted; + // webView2.NavigationStarting += NavigationStarting_CancelNavigation; + // Content = webView2; + // } + // } + private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) + { + // 调用外部设置的导航完成 Action + OnNavigationCompletedAction?.Invoke(e); + } + private void WebView_CoreWebView2InitializationCompleted(object? sender, CoreWebView2InitializationCompletedEventArgs e) { if (e.IsSuccess) @@ -143,6 +174,15 @@ public class WebpagePanel : UserControl })); } + public void NavigateToMd(string md, string backgroundColor = "#2b2b2b") + { + md = WebUtility.HtmlEncode(md); + string md2Html = ResourceHelper.GetString($"pack://application:,,,/Assets/Strings/md2html.html", + Encoding.UTF8); + var html = md2Html.Replace("{{content}}", md).Replace("#202020", backgroundColor); + NavigateToHtml(html); + } + private void NavigationStarting_CancelNavigation(object? sender, CoreWebView2NavigationStartingEventArgs e) { if (e.Uri.StartsWith("data:")) // when using NavigateToString diff --git a/BetterGenshinImpact/View/MainWindow.xaml b/BetterGenshinImpact/View/MainWindow.xaml index 4da1f4e3..43a50336 100644 --- a/BetterGenshinImpact/View/MainWindow.xaml +++ b/BetterGenshinImpact/View/MainWindow.xaml @@ -43,9 +43,15 @@ + + + + diff --git a/BetterGenshinImpact/View/Pages/JsListPage.xaml b/BetterGenshinImpact/View/Pages/JsListPage.xaml index 30322c99..b8613ad5 100644 --- a/BetterGenshinImpact/View/Pages/JsListPage.xaml +++ b/BetterGenshinImpact/View/Pages/JsListPage.xaml @@ -7,6 +7,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" + xmlns:drawer="clr-namespace:BetterGenshinImpact.View.Controls.Drawer" d:DataContext="{d:DesignInstance Type=pages:JsListViewModel}" d:DesignHeight="600" d:DesignWidth="800" @@ -22,82 +23,93 @@ - - - - - - - - - - - - 可以通过 Javascript 调用 BetterGI 在原神中的各项能力。请在调度器中使用! - 点击查看 Javascript 脚本使用与编写教程 - - + + + + + + + + + + - - - - - - 脚本仓库 - - - - + + + 可以通过 Javascript 调用 BetterGI 在原神中的各项能力。请在调度器中使用! + + 点击查看 Javascript 脚本使用与编写教程 + + - + + + + + + 脚本仓库 + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/MapPathingPage.xaml b/BetterGenshinImpact/View/Pages/MapPathingPage.xaml index 0f893f25..64d0e092 100644 --- a/BetterGenshinImpact/View/Pages/MapPathingPage.xaml +++ b/BetterGenshinImpact/View/Pages/MapPathingPage.xaml @@ -10,6 +10,7 @@ xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:vio="http://schemas.lepo.co/wpfui/2022/xaml/violeta" + xmlns:drawer="clr-namespace:BetterGenshinImpact.View.Controls.Drawer" d:DataContext="{d:DesignInstance Type=pages:MapPathingViewModel}" d:DesignHeight="600" d:DesignWidth="800" @@ -25,102 +26,135 @@ - - - - - - - - - + + + + + + + + + + - - - 可以实现自动采集、自动挖矿、自动锄地等功能。请在调度器中使用! - 点击查看地图追踪与录制使用教程 - - + + + 可以实现自动采集、自动挖矿、自动锄地等功能。请在调度器中使用! + + 点击查看地图追踪与录制使用教程 + + - - - - - 脚本仓库 - + + + + + 脚本仓库 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Pages/OneDragonFlowPage.xaml b/BetterGenshinImpact/View/Pages/OneDragonFlowPage.xaml index 594bf7d2..e2733aee 100644 --- a/BetterGenshinImpact/View/Pages/OneDragonFlowPage.xaml +++ b/BetterGenshinImpact/View/Pages/OneDragonFlowPage.xaml @@ -335,13 +335,18 @@ - + + + + + - - + @@ -470,7 +475,8 @@ - + + @@ -502,25 +508,20 @@ - - - - - + + + - 新的一天开始于 4:00 - - - 周一 4:00 至周二 3:59 执行周一配置,以此类推。 - + @@ -841,6 +842,9 @@ TextWrapping="Wrap" /> @@ -852,6 +856,9 @@ Margin="0,0,28,0" Text="{Binding SelectedConfig.DailyRewardPartyName, Mode=TwoWay}" PlaceholderText="填写好感队名称" + TextAlignment="Center" + VerticalAlignment="Center" + HorizontalAlignment="Center" TextWrapping="Wrap" /> diff --git a/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml b/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml index 6a243f23..800eea33 100644 --- a/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml +++ b/BetterGenshinImpact/View/Pages/TaskSettingsPage.xaml @@ -9,6 +9,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" + xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf" Title="TaskSettingsPage" d:DataContext="{d:DesignInstance Type=pages:TaskSettingsPageViewModel}" d:DesignHeight="1150" @@ -954,20 +955,113 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml b/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml index 77d37e58..e649dfa6 100644 --- a/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml +++ b/BetterGenshinImpact/View/Pages/View/ScriptGroupConfigView.xaml @@ -878,6 +878,32 @@ Margin="0,0,36,0" IsChecked="{Binding PathingConfig.AutoFightConfig.KazuhaPickupEnabled, Mode=TwoWay}" /> + + + + + + + + + + + + + diff --git a/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs b/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs index aba149f9..bdd5abad 100644 --- a/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs +++ b/BetterGenshinImpact/View/Windows/CheckUpdateWindow.xaml.cs @@ -135,11 +135,11 @@ public partial class CheckUpdateWindow : FluentWindow if (_option.Channel == UpdateChannel.Stable) { - await RunUpdaterAsync("--source mirrorc"); + await RunUpdaterAsync("-I --source mirrorc"); } else { - await RunUpdaterAsync("--source mirrorc-alpha"); + await RunUpdaterAsync("-I --source mirrorc-alpha"); } } diff --git a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml new file mode 100644 index 00000000..cb194d67 --- /dev/null +++ b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs new file mode 100644 index 00000000..2c84b9b0 --- /dev/null +++ b/BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using BetterGenshinImpact.Core.Config; +using BetterGenshinImpact.Core.Script; +using BetterGenshinImpact.GameTask; +using BetterGenshinImpact.Helpers; +using Wpf.Ui.Violeta.Controls; + +namespace BetterGenshinImpact.View.Windows; + +[ObservableObject] +public partial class ScriptRepoWindow +{ + // 更新渠道类 + public class RepoChannel + { + public string Name { get; set; } + public string Url { get; set; } + + public RepoChannel(string name, string url) + { + Name = name; + Url = url; + } + } + + // 渠道列表 + private ObservableCollection _repoChannels; + public ObservableCollection RepoChannels => _repoChannels; + + // 选中的渠道 + [ObservableProperty] private RepoChannel _selectedRepoChannel; + + public ScriptRepoWindow() + { + InitializeRepoChannels(); + InitializeComponent(); + DataContext = this; + } + + private void InitializeRepoChannels() + { + _repoChannels = new ObservableCollection + { + new RepoChannel("CNB", "https://cnb.cool/bettergi/bettergi-scripts-list"), + new RepoChannel("GitCode", "https://gitcode.com/huiyadanli/bettergi-scripts-list"), + new RepoChannel("Gitee", "https://gitee.com/babalae/bettergi-scripts-list"), + new RepoChannel("GitHub", "https://github.com/babalae/bettergi-scripts-list"), + }; + SelectedRepoChannel = _repoChannels[0]; + } + + [RelayCommand] + private async Task UpdateRepo() + { + try + { + // 使用选定渠道的URL进行更新 + string repoUrl = SelectedRepoChannel.Url; + + // 显示更新中提示 + Toast.Information("正在更新脚本仓库..."); + + // 执行更新 + var (repoPath, updated) = await ScriptRepoUpdater.Instance.UpdateCenterRepoByGit(repoUrl); + + // 更新结果提示 + if (updated) + { + Toast.Success("脚本仓库更新成功,有新内容"); + } + else + { + Toast.Success("脚本仓库已是最新"); + } + } + catch (Exception ex) + { + await MessageBox.ErrorAsync($"更新失败,可尝试重置仓库后重新更新。失败原因:: {ex.Message}"); + } + } + + [RelayCommand] + private void OpenLocalScriptRepo() + { + TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false; + ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + Close(); + } + + [RelayCommand] + private async Task ResetRepo() + { + // 添加确认对话框 + var result = await MessageBox.ShowAsync( + "确定要重置脚本仓库吗?无法正常更新时候可以使用本功能,重置后请重新更新脚本仓库。", + "确认重置", + MessageBoxButton.YesNo, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Yes) + { + try + { + if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) + { + DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPath); + Toast.Success("脚本仓库已重置,请重新更新脚本仓库。"); + } + else + { + Toast.Information("脚本仓库不存在,无需重置"); + } + } + catch (Exception ex) + { + Toast.Error($"重置失败: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs index 1e139318..21d7d4ca 100644 --- a/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs +++ b/BetterGenshinImpact/ViewModel/MainWindowViewModel.cs @@ -149,7 +149,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel } // 更新仓库 - ScriptRepoUpdater.Instance.AutoUpdate(); + // ScriptRepoUpdater.Instance.AutoUpdate(); // 清理临时目录 TempManager.CleanUp(); diff --git a/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs index aa1b39dc..019a43d8 100644 --- a/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/CommonSettingsPageViewModel.cs @@ -226,15 +226,16 @@ public partial class CommonSettingsPageViewModel : ViewModel { var zipPath = dialog.FileName; // 删除旧文件夹 - if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) + if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld)) { - DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPath); + DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPathOld); } ZipFile.ExtractToDirectory(zipPath, ScriptRepoUpdater.ReposPath, true); - if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath)) + if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld)) { + DirectoryHelper.CopyDirectory(ScriptRepoUpdater.CenterRepoPathOld, ScriptRepoUpdater.CenterRepoPath); MessageBox.Information("脚本仓库离线包导入成功!"); } else diff --git a/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs index c5528bb6..889b6417 100644 --- a/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/JsListViewModel.cs @@ -9,15 +9,25 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; using BetterGenshinImpact.Helpers; +using BetterGenshinImpact.View.Controls.Drawer; +using BetterGenshinImpact.View.Controls.Webview; using BetterGenshinImpact.ViewModel.Message; using CommunityToolkit.Mvvm.Messaging; +using Microsoft.Web.WebView2.Wpf; using Wpf.Ui; -using Wpf.Ui.Controls; using Wpf.Ui.Violeta.Controls; +using Button = Wpf.Ui.Controls.Button; +using StackPanel = Wpf.Ui.Controls.StackPanel; +using TextBlock = Wpf.Ui.Controls.TextBlock; +using TextBox = Wpf.Ui.Controls.TextBox; namespace BetterGenshinImpact.ViewModel.Pages; @@ -26,13 +36,21 @@ public partial class JsListViewModel : ViewModel private readonly ILogger _logger = App.GetLogger(); private readonly string scriptPath = Global.ScriptPath(); - [ObservableProperty] - private ObservableCollection _scriptItems = []; + [ObservableProperty] private ObservableCollection _scriptItems = []; private readonly IScriptService _scriptService; public AllConfig Config { get; set; } + public DrawerViewModel DrawerVm { get; } = new DrawerViewModel(); + + private WebView2? _webView2; + + private WebpagePanel? _mdWebpagePanel; + + private TaskCompletionSource? _navigationCompletionSource; + private const int NavigationTimeoutMs = 10000; // 10秒超时 + public JsListViewModel(IScriptService scriptService, IConfigService configService) { _scriptService = scriptService; @@ -114,13 +132,214 @@ public partial class JsListViewModel : ViewModel [RelayCommand] public void OnGoToJsScriptUrl() { - Process.Start(new ProcessStartInfo("https://bettergi.com/feats/autos/jsscript.html") { UseShellExecute = true }); + Process.Start(new ProcessStartInfo("https://bettergi.com/feats/autos/jsscript.html") + { UseShellExecute = true }); } [RelayCommand] public void OnOpenLocalScriptRepo() { Config.ScriptConfig.ScriptRepoHintDotVisible = false; - ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + ScriptRepoUpdater.Instance.OpenScriptRepoWindow(); } -} + + [RelayCommand] + private void OpenScriptDetailDrawer(object? scriptItem) + { + if (scriptItem == null) + { + return; + } + + if (scriptItem is ScriptProject scriptProject) + { + // 检查是否存在README.md或其他md文件 + var mdFilePath = FindMdFilePath(scriptProject); + + // 设置抽屉位置和大小 + DrawerVm.DrawerPosition = DrawerPosition.Right; + + if (!string.IsNullOrEmpty(mdFilePath)) + { + DrawerVm.DrawerWidth = 450; + // 注册抽屉关闭前事件 + DrawerVm.SetDrawerClosingAction(args => + { + if (_mdWebpagePanel != null) + { + _mdWebpagePanel.Visibility = Visibility.Hidden; + } + }); + DrawerVm.setDrawerOpenedAction(async () => + { + if (_mdWebpagePanel != null) + { + // 等待导航完成或超时 + try + { + await WaitForNavigationCompletedWithTimeout(); + _mdWebpagePanel.Visibility = Visibility.Visible; + _mdWebpagePanel.WebView.Focus(); + Debug.WriteLine("Navigation completed successfully"); + // 导航成功完成后执行其他操作 + } + catch (TimeoutException) + { + Toast.Error("Markdown内容加载超时"); + } + } + }); + } + else + { + DrawerVm.SetDrawerClosingAction(_ => { }); + DrawerVm.setDrawerOpenedAction(() => { }); + DrawerVm.DrawerWidth = 300; + } + + // 创建要在抽屉中显示的内容 + var content = CreateScriptDetailContent(scriptProject, mdFilePath); + + // 打开抽屉 + DrawerVm.OpenDrawer(content); + } + } + + private async Task WaitForNavigationCompletedWithTimeout() + { + var completedTask = await Task.WhenAny( + _navigationCompletionSource!.Task, + Task.Delay(NavigationTimeoutMs) + ); + + if (completedTask != _navigationCompletionSource.Task) + { + throw new TimeoutException("Navigation did not complete within the timeout period"); + } + } + + private object CreateScriptDetailContent(ScriptProject scriptProject, string? mdFilePath) + { + // 创建显示脚本详情的控件 + var border = new Border + { + Background = new SolidColorBrush(Color.FromRgb(0x2B, 0x2B, 0x2B)), + Padding = new Thickness(20) + }; + var panel = new StackPanel(); + border.Child = panel; + + // 假设scriptItem是你的脚本对象,根据实际类型进行调整 + panel.Children.Add(new TextBlock + { + Text = scriptProject.Manifest.Name, + FontSize = 20, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 0, 0, 10) + }); + + + // 如果找到md文件,使用WebpagePanel显示 + if (!string.IsNullOrEmpty(mdFilePath)) + { + // 使用Grid作为容器来实现填充效果 + var grid = new Grid + { + Margin = new Thickness(0, 0, 0, 15) + }; + + _mdWebpagePanel = new WebpagePanel + { + Margin = new Thickness(0), + Visibility = Visibility.Hidden, + VerticalAlignment = VerticalAlignment.Stretch, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + _navigationCompletionSource = new TaskCompletionSource(); + _mdWebpagePanel.OnNavigationCompletedAction = (_) => + { + // 导航完成时设置任务结果 + _navigationCompletionSource.TrySetResult(true); + }; + _mdWebpagePanel.NavigateToMd(File.ReadAllText(mdFilePath)); + + grid.Children.Add(_mdWebpagePanel); + panel.Children.Add(grid); + + // 设置Grid高度以占满剩余空间 + panel.SizeChanged += (sender, args) => + { + // 计算其他元素使用的高度 + double otherElementsHeight = 0; + foreach (var child in panel.Children) + { + if (child != grid) + { + var frameworkElement = child as FrameworkElement; + if (frameworkElement != null) + { + otherElementsHeight += frameworkElement.ActualHeight + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom; + } + } + } + + // 设置Grid高度为剩余空间 + grid.Height = Math.Max(400, panel.ActualHeight - otherElementsHeight - 15); // 设置最小高度为400 + }; + } + else + { + panel.Children.Add(new TextBlock + { + Text = $"版本: {scriptProject.Manifest.Version}", + Margin = new Thickness(0, 5, 0, 5) + }); + + panel.Children.Add(new TextBlock + { + Text = scriptProject.Manifest.Description, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 5, 0, 15) + }); + } + + // 添加操作按钮 + // var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal }; + // + // var runButton = new Button + // { + // Content = "执行脚本", + // Margin = new Thickness(0, 0, 10, 0) + // }; + // runButton.Click += async (s, e) => await OnStartRun(script); + // buttonPanel.Children.Add(runButton); + // + // var openFolderButton = new Button { Content = "打开目录" }; + // openFolderButton.Click += (s, e) => OnOpenScriptProjectFolder(script); + // buttonPanel.Children.Add(openFolderButton); + + // panel.Children.Add(buttonPanel); + + + return border; + } + + private static string? FindMdFilePath(ScriptProject script) + { + string[] possibleMdFiles = { "README.md", "readme.md" }; + string mdFilePath = null; + + foreach (var mdFile in possibleMdFiles) + { + string fullPath = Path.Combine(script.ProjectPath, mdFile); + if (File.Exists(fullPath)) + { + mdFilePath = fullPath; + break; + } + } + + return mdFilePath; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/KeyMouseRecordPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/KeyMouseRecordPageViewModel.cs index 393d5ca0..23b9ea27 100644 --- a/BetterGenshinImpact/ViewModel/Pages/KeyMouseRecordPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/KeyMouseRecordPageViewModel.cs @@ -231,6 +231,6 @@ public partial class KeyMouseRecordPageViewModel : ViewModel public void OnOpenLocalScriptRepo() { Config.ScriptConfig.ScriptRepoHintDotVisible = false; - ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + ScriptRepoUpdater.Instance.OpenScriptRepoWindow(); } } diff --git a/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs index cee4d6f6..2e9ce69e 100644 --- a/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/MapPathingViewModel.cs @@ -1,4 +1,5 @@ -using BetterGenshinImpact.Core.Config; +using System; +using BetterGenshinImpact.Core.Config; using BetterGenshinImpact.Core.Script.Group; using BetterGenshinImpact.Core.Script.Project; using BetterGenshinImpact.GameTask.AutoPathing; @@ -21,6 +22,14 @@ using Wpf.Ui.Violeta.Controls; using BetterGenshinImpact.View.Pages.View; using BetterGenshinImpact.ViewModel.Pages.View; using Wpf.Ui.Violeta.Win32; +using BetterGenshinImpact.View.Controls.Drawer; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.ComponentModel; +using BetterGenshinImpact.View.Controls.Webview; +using Microsoft.Web.WebView2.Wpf; +using BetterGenshinImpact.Helpers; namespace BetterGenshinImpact.ViewModel.Pages; @@ -39,6 +48,15 @@ public partial class MapPathingViewModel : ViewModel private readonly IScriptService _scriptService; public AllConfig Config { get; set; } + + // 添加抽屉ViewModel + public DrawerViewModel DrawerVm { get; } = new DrawerViewModel(); + + // 添加WebView2相关成员变量 + private WebView2? _webView2; + private WebpagePanel? _mdWebpagePanel; + private TaskCompletionSource? _navigationCompletionSource; + private const int NavigationTimeoutMs = 10000; // 10秒超时 /// public MapPathingViewModel(IScriptService scriptService, IConfigService configService) @@ -179,6 +197,222 @@ public partial class MapPathingViewModel : ViewModel public void OnOpenLocalScriptRepo() { Config.ScriptConfig.ScriptRepoHintDotVisible = false; - ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + ScriptRepoUpdater.Instance.OpenScriptRepoWindow(); } -} + + [RelayCommand] + public void OnOpenPathingDetail() + { + var item = SelectNode; + if (item == null) + { + return; + } + + // 如果是目录,检查是否存在README.md + string? mdFilePath = null; + if (item.IsDirectory && !string.IsNullOrEmpty(item.FilePath)) + { + mdFilePath = FindMdFilePath(item.FilePath); + } + + // 设置抽屉位置和大小 + DrawerVm.DrawerPosition = DrawerPosition.Right; + + if (!string.IsNullOrEmpty(mdFilePath)) + { + DrawerVm.DrawerWidth = 450; + // 注册抽屉关闭前事件 + DrawerVm.SetDrawerClosingAction(args => + { + if (_mdWebpagePanel != null) + { + _mdWebpagePanel.Visibility = Visibility.Hidden; + } + }); + DrawerVm.setDrawerOpenedAction(async () => + { + if (_mdWebpagePanel != null) + { + // 等待导航完成或超时 + try + { + await WaitForNavigationCompletedWithTimeout(); + _mdWebpagePanel.Visibility = Visibility.Visible; + _mdWebpagePanel.WebView.Focus(); + Debug.WriteLine("Navigation completed successfully"); + } + catch (TimeoutException) + { + Toast.Error("Markdown内容加载超时"); + } + } + }); + } + else + { + DrawerVm.DrawerWidth = 350; + DrawerVm.SetDrawerClosingAction(_ => { }); + DrawerVm.setDrawerOpenedAction(() => { }); + } + + // 创建要在抽屉中显示的内容 + var content = CreatePathingDetailContent(item, mdFilePath); + + // 打开抽屉 + if (content != null) + { + DrawerVm.OpenDrawer(content); + } + } + + private async Task WaitForNavigationCompletedWithTimeout() + { + var completedTask = await Task.WhenAny( + _navigationCompletionSource!.Task, + Task.Delay(NavigationTimeoutMs) + ); + + if (completedTask != _navigationCompletionSource.Task) + { + throw new TimeoutException("Navigation did not complete within the timeout period"); + } + } + + private string? FindMdFilePath(string dirPath) + { + string[] possibleMdFiles = { "README.md", "readme.md" }; + + foreach (var mdFile in possibleMdFiles) + { + string fullPath = Path.Combine(dirPath, mdFile); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + + return null; + } + + private object? CreatePathingDetailContent(FileTreeNode node, string? mdFilePath = null) + { + // 创建显示路径任务详情的控件 + var border = new Border + { + Background = new SolidColorBrush(Color.FromRgb(0x2B, 0x2B, 0x2B)), + Padding = new Thickness(20) + }; + + var panel = new StackPanel(); + border.Child = panel; + + // 添加标题 + panel.Children.Add(new TextBlock + { + Text = node.FileName, + FontSize = 20, + FontWeight = FontWeights.Bold, + Margin = new Thickness(0, 0, 0, 10) + }); + + // 如果找到md文件,使用WebpagePanel显示 + if (!string.IsNullOrEmpty(mdFilePath)) + { + // 使用Grid作为容器来实现填充效果 + var grid = new Grid + { + Margin = new Thickness(0, 0, 0, 15) + }; + + _mdWebpagePanel = new WebpagePanel + { + Margin = new Thickness(0), + Visibility = Visibility.Hidden, + VerticalAlignment = VerticalAlignment.Stretch, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + _navigationCompletionSource = new TaskCompletionSource(); + _mdWebpagePanel.OnNavigationCompletedAction = (_) => + { + // 导航完成时设置任务结果 + _navigationCompletionSource.TrySetResult(true); + }; + _mdWebpagePanel.NavigateToMd(File.ReadAllText(mdFilePath)); + + grid.Children.Add(_mdWebpagePanel); + panel.Children.Add(grid); + + // 设置Grid高度以占满剩余空间 + panel.SizeChanged += (sender, args) => + { + // 计算其他元素使用的高度 + double otherElementsHeight = 0; + foreach (var child in panel.Children) + { + if (child != grid) + { + var frameworkElement = child as FrameworkElement; + if (frameworkElement != null) + { + otherElementsHeight += frameworkElement.ActualHeight + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom; + } + } + } + + // 设置Grid高度为剩余空间 + grid.Height = Math.Max(400, panel.ActualHeight - otherElementsHeight - 15); // 设置最小高度为400 + }; + } + else if (!node.IsDirectory && !string.IsNullOrEmpty(node.FilePath)) + { + // 如果是文件而不是目录,显示更多详情 + try + { + if (string.IsNullOrEmpty(node.Value?.Info.Description)) + { + return null; + } + + panel.Children.Add(new TextBlock + { + Text = $"{node.Value?.Info.Description}", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 5, 0, 5) + }); + } + catch (Exception ex) + { + panel.Children.Add(new TextBlock + { + Text = $"读取文件信息时出错: {ex.Message}", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 5, 0, 5) + }); + } + } + else + { + // 显示目录信息 + panel.Children.Add(new TextBlock + { + Text = "这是一个目录,包含多个地图追踪任务。", + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 5, 0, 15) + }); + + // 添加子项信息 + if (node.Children.Count > 0) + { + panel.Children.Add(new TextBlock + { + Text = $"包含 {node.Children.Count} 个子项", + Margin = new Thickness(0, 5, 0, 5) + }); + } + } + + return border; + } +} \ No newline at end of file diff --git a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs index 34234995..e965c9f7 100644 --- a/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/ScriptControlViewModel.cs @@ -451,7 +451,7 @@ public partial class ScriptControlViewModel : ViewModel public void OnOpenLocalScriptRepo() { TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false; - ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + ScriptRepoUpdater.Instance.OpenScriptRepoWindow(); } [RelayCommand] @@ -1016,20 +1016,31 @@ public partial class ScriptControlViewModel : ViewModel } [RelayCommand] - public void OnDeleteScriptByFolder(ScriptGroupProject? item) + public async void OnDeleteScriptByFolder(ScriptGroupProject? item) { if (item == null) { return; - } - - var toBeDeletedProjects = SelectedScriptGroup?.Projects.ToList().Where(item2 => item2.FolderName == item.FolderName); - if (toBeDeletedProjects != null) + } + + if (SelectedScriptGroup != null) { + var toBeDeletedProjects = SelectedScriptGroup.Projects + .Where(item2 => item2.FolderName == item.FolderName) + .ToList(); + foreach (var project in toBeDeletedProjects) { - OnDeleteScript(project); - } + SelectedScriptGroup.Projects.Remove(project); + } + + _snackbarService.Show( + "脚本配置移除成功", + $"已移除 {item.FolderName} 下的所有关联配置", + ControlAppearance.Success, + null, + TimeSpan.FromSeconds(2) + ); } } diff --git a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs index 02eeb6c9..33e592c6 100644 --- a/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/TaskSettingsPageViewModel.cs @@ -31,6 +31,7 @@ using BetterGenshinImpact.ViewModel.Pages.View; using System.Linq; using System.Reflection; using System.Collections.Frozen; +using System.Diagnostics; using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.View.Windows; @@ -162,13 +163,16 @@ public partial class TaskSettingsPageViewModel : ViewModel [RelayCommand] private async Task OnSOneDragonFlow() { - if (OneDragonFlowViewModel == null || OneDragonFlowViewModel.SelectedConfig == null) - { - Toast.Warning("未设置任务!"); - return; - } - OneDragonFlowViewModel.OnNavigatedTo(); - await OneDragonFlowViewModel.OnOneKeyExecute(); + if (OneDragonFlowViewModel == null || OneDragonFlowViewModel.SelectedConfig == null) + { + OneDragonFlowViewModel.OnNavigatedTo(); + if (OneDragonFlowViewModel == null || OneDragonFlowViewModel.SelectedConfig == null) + { + Toast.Warning("未设置任务!"); + return; + } + } + await OneDragonFlowViewModel.OnOneKeyExecute(); } [RelayCommand] diff --git a/BetterGenshinImpact/ViewModel/Pages/View/AutoFightViewModel.cs b/BetterGenshinImpact/ViewModel/Pages/View/AutoFightViewModel.cs index 428271f4..d9e8f3f1 100644 --- a/BetterGenshinImpact/ViewModel/Pages/View/AutoFightViewModel.cs +++ b/BetterGenshinImpact/ViewModel/Pages/View/AutoFightViewModel.cs @@ -79,7 +79,7 @@ public partial class AutoFightViewModel : ObservableObject, IViewModel public void OnOpenLocalScriptRepo() { Config.ScriptConfig.ScriptRepoHintDotVisible = false; - ScriptRepoUpdater.Instance.OpenLocalRepoInWebView(); + ScriptRepoUpdater.Instance.OpenScriptRepoWindow(); } [RelayCommand] diff --git a/Build/kachina.config.json b/Build/kachina.config.json index 7969f5bc..97c0e861 100644 --- a/Build/kachina.config.json +++ b/Build/kachina.config.json @@ -8,12 +8,12 @@ { "id": "mirrorc", "name": "Mirror酱", - "uri": "mirrorc://BGI" + "uri": "mirrorc://BGI?os=win&arch=x64" }, { "id": "mirrorc-alpha", "name": "Mirror酱 Alpha", - "uri": "mirrorc://BGI?channel=alpha", + "uri": "mirrorc://BGI?channel=alpha&os=win&arch=x64", "hidden": true } ], diff --git a/README.md b/README.md index 0042f7b2..b3ad734a 100644 --- a/README.md +++ b/README.md @@ -130,4 +130,4 @@ BetterGI · 更好的原神, 一个基于计算机视觉技术,意图让原 ## 问题反馈 -提 [Issue](https://github.com/babalae/better-genshin-impact/issues) 或 QQ群[1036100501](https://qm.qq.com/q/fvRNqEbFyo) +提 [Issue](https://github.com/babalae/better-genshin-impact/issues) 或 QQ群[1053273766](https://qm.qq.com/q/qtocsOXnIQ) diff --git a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs index 3f61f6c9..5eee0029 100644 --- a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs +++ b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyGen.cs @@ -16,6 +16,8 @@ public class AvatarClassifyGen private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\数据源\background"; private static readonly Random Rd = new Random(); + + public static readonly List ImgNames = ["UI_AvatarIcon_Side_Ambor.png","UI_AvatarIcon_Side_AmborCostumeWic.png"]; public static void GenAll() { @@ -23,9 +25,8 @@ public class AvatarClassifyGen // List sideImageFiles = Directory.GetFiles(Path.Combine(BaseDir, "side_src"), "*.png", SearchOption.TopDirectoryOnly).ToList(); // 只用一个图像 List sideImageFiles = []; - List imgNames = ["UI_AvatarIcon_Side_Escoffier.png","UI_AvatarIcon_Side_Ifa.png", - "UI_AvatarIcon_Side_Flamingo.png","UI_AvatarIcon_Side_Hookwalker.png","UI_AvatarIcon_Side_Mosasaurus.png","UI_AvatarIcon_Side_Shamansaurus.png","UI_AvatarIcon_Side_Drillhead.png"]; - foreach (string imgName in imgNames) + + foreach (string imgName in ImgNames) { sideImageFiles.Add(Path.Combine(BaseDir, imgName)); } diff --git a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs index e0475b65..b1aca700 100644 --- a/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs +++ b/Test/BetterGenshinImpact.Test/Dataset/AvatarClassifyTransparentGen.cs @@ -23,12 +23,7 @@ public class AvatarClassifyTransparentGen public static void GenAll() { List sideImageFiles = []; - List imgNames = - [ - "UI_AvatarIcon_Side_Escoffier.png", "UI_AvatarIcon_Side_Ifa.png", - "UI_AvatarIcon_Side_Flamingo.png", "UI_AvatarIcon_Side_Hookwalker.png", "UI_AvatarIcon_Side_Mosasaurus.png", - "UI_AvatarIcon_Side_Shamansaurus.png", "UI_AvatarIcon_Side_Drillhead.png" - ]; + List imgNames = AvatarClassifyGen.ImgNames; foreach (string imgName in imgNames) { sideImageFiles.Add(Path.Combine(BaseDir, imgName)); diff --git a/Test/BetterGenshinImpact.Test/Simple/OcrTest.cs b/Test/BetterGenshinImpact.Test/Simple/OcrTest.cs index 0985abe1..b0409ee0 100644 --- a/Test/BetterGenshinImpact.Test/Simple/OcrTest.cs +++ b/Test/BetterGenshinImpact.Test/Simple/OcrTest.cs @@ -10,13 +10,13 @@ public class OcrTest { public static void TestYap() { - Mat mat = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png"); - var text = TextInferenceFactory.Pick.Inference(TextInferenceFactory.PreProcessForInference(mat)); - Debug.WriteLine(text); - - Mat mat2 = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png"); - var text2 = OcrFactory.Paddle.Ocr(mat2); - Debug.WriteLine(text2); + // Mat mat = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png"); + // var text = TextInferenceFactory.Pick.Inference(TextInferenceFactory.PreProcessForInference(mat)); + // Debug.WriteLine(text); + // + // Mat mat2 = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png"); + // var text2 = OcrFactory.Paddle.Ocr(mat2); + // Debug.WriteLine(text2); } diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs index 7f67f71a..7f01e39e 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoArtifactSalvageTests/AutoArtifactSalvageTaskTests.cs @@ -1,5 +1,6 @@ using BetterGenshinImpact.GameTask.AutoArtifactSalvage; using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests; +using GameTask.Model.GameUI; using OpenCvSharp; using System; using System.Collections.Concurrent; @@ -52,7 +53,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}"); // - var result = AutoArtifactSalvageTask.GetArtifactGridItems(mat); + var result = GridScreen.GridEnumerator.GetGridItems(mat); // Assert.Equal(4, result.Count()); @@ -69,7 +70,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\ArtifactGrid.png"); // - var result = AutoArtifactSalvageTask.GetArtifactGridItems(mat); + var result = GridScreen.GridEnumerator.GetGridItems(mat); using Mat leftTopOne = new Mat(mat, result.Single(r => r.X + r.Width / 2 < mat.Width / 2 && r.Y + r.Height / 2 < mat.Height / 2)); using Mat rightTopOne = new Mat(mat, result.Single(r => r.X + r.Width / 2 > mat.Width / 2 && r.Y + r.Height / 2 < mat.Height / 2)); using Mat leftBottomOne = new Mat(mat, result.Single(r => r.X + r.Width / 2 < mat.Width / 2 && r.Y + r.Height / 2 > mat.Height / 2)); diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.Training.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.Training.cs new file mode 100644 index 00000000..86a8ed1d --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.Training.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static TorchSharp.torch.nn; +using static TorchSharp.torch; +using TorchSharp; +using System.Diagnostics; +using System.Collections; +using BetterGenshinImpact.GameTask.AutoFishing; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests +{ + public partial class RodNetTests + { + /// + /// RodNet验证,应在数据集上达到一定准确率 + /// + [Theory] + [InlineData(@"..\..\..\Assets\AutoFishing\data_selected.csv")] + public void Training_AccuracyShouldBeOK(string dataLocation) + { + // + using var _ = no_grad(); + + var device = + torch.cuda.is_available() ? torch.CUDA : + torch.mps_is_available() ? torch.MPS : + torch.CPU; + var loss = CrossEntropyLoss(); + var sut = new RodNet().to((Device)device); + sut.SetWeightsManually(); + + using var test_reader = new CSVReader(Enumerable.Repeat(false, 8).Concat(Enumerable.Repeat(true, 2)), Path.GetFullPath(dataLocation), (Device)device); + + // + var accuracy = evaluate(test_reader.GetBatches(eval_batch_size), sut, loss); + + // + Assert.True(accuracy > 0.8); + } + + /// + /// RodNet必须粗略地支持训练 + /// + [Fact] + public void Training_ShouldBeDifferentiable() + { + // + RodInput input = new RodInput(); + var (y0, z0, t, u, v, h) = RodNet.GetRodStatePreProcess(input); + + Tensor fishLabel = tensor(new double[] { input.fish_label }, dtype: ScalarType.Int32); + Tensor uv = tensor(new double[,] { { u, v } }, dtype: ScalarType.Float64); + Tensor y0z0t = tensor(new double[,] { { y0, z0, t } }, dtype: ScalarType.Float64); + Tensor h_ = tensor(new double[,] { { h } }, dtype: ScalarType.Float64); + RodNet sut = new RodNet(); + + // + Tensor output = sut.forward(fishLabel, uv, y0z0t, h_); + output.backward([torch.ones_like(output)]); + // + } + + #region 训练相关代码 + // 这部分代码改编自TorchSharpExamples的CSharpExamples.TextClassification + private const long batch_size = 32; + private const long eval_batch_size = 32; + + internal static RodNet Run(int epochs, int timeout, string dataLocation) + { + torch.random.manual_seed(1); + + var device = + torch.cuda.is_available() ? torch.CUDA : + torch.mps_is_available() ? torch.MPS : + torch.CPU; + + Console.WriteLine(); + Console.WriteLine($"\tRunning TextClassification on {device.type.ToString()} for {epochs} epochs, terminating after {TimeSpan.FromSeconds(timeout)}."); + Console.WriteLine(); + + Console.WriteLine($"\tPreparing training and test data..."); + + using (var reader = new CSVReader(Enumerable.Repeat(true, 8).Concat(Enumerable.Repeat(false, 2)), dataLocation, (Device)device)) + { + Console.WriteLine($"\tCreating the model..."); + Console.WriteLine(); + + var model = new RodNet().to((Device)device); + + var loss = CrossEntropyLoss(); + var lr = 1e-2; + var optimizer = torch.optim.SGD(model.parameters(), learningRate: lr); + var scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, epochs, eta_min: 0); + + var totalTime = new Stopwatch(); + totalTime.Start(); + + foreach (var epoch in Enumerable.Range(1, epochs)) + { + + var sw = new Stopwatch(); + sw.Start(); + + train(epoch, reader.GetBatches(batch_size), model, loss, optimizer); + + sw.Stop(); + + Console.WriteLine($"\nEnd of epoch: {epoch} | lr: {optimizer.ParamGroups.First().LearningRate:0.0000} | time: {sw.Elapsed.TotalSeconds:0.0}s\n"); + scheduler.step(); + + if (totalTime.Elapsed.TotalSeconds > timeout) break; + } + + totalTime.Stop(); + + using (var test_reader = new CSVReader(Enumerable.Repeat(false, 8).Concat(Enumerable.Repeat(true, 2)), dataLocation, (Device)device)) + { + + var sw = new Stopwatch(); + sw.Start(); + + var accuracy = evaluate(test_reader.GetBatches(eval_batch_size), model, loss); + + sw.Stop(); + + Console.WriteLine($"\nEnd of training: test accuracy: {accuracy:0.00} | eval time: {sw.Elapsed.TotalSeconds:0.0}s\n"); + scheduler.step(); + } + + foreach (var (name, param) in model.named_parameters()) + { + switch (param.dtype) + { + case ScalarType.Int64: + Console.WriteLine($"参数{name}={String.Join(", ", param.data())}"); + break; + case ScalarType.Float32: + Console.WriteLine($"参数{name}={String.Join(", ", param.data())}"); + break; + case ScalarType.Float64: + Console.WriteLine($"参数{name}={String.Join(", ", param.data())}"); + break; + } + } + + return model; + } + + } + + static void train(int epoch, IEnumerable<(Tensor, Tensor, Tensor, Tensor, Tensor)> train_data, RodNet model, Loss criterion, torch.optim.Optimizer optimizer) + { + model.train(); + + double total_acc = 0.0; + long total_count = 0; + long log_interval = 1; + + var batch = 0; + + var batch_count = train_data.Count(); + + using (var d = torch.NewDisposeScope()) + { + foreach (var (y0z0t, uv, h, fish_label, success) in train_data) + { + + optimizer.zero_grad(); + + using (var predicted_labels = model.forward(fish_label, uv, y0z0t, h)) + { + var loss = criterion.forward(predicted_labels, success.to(ScalarType.Int64)); + loss.backward(); + torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1); + optimizer.step(); + + total_acc += (predicted_labels.argmax(1) == success).sum().to(torch.CPU).item(); + total_count += success.size(0); + } + + batch += 1; + if (batch % log_interval == 0) + { + var accuracy = total_acc / total_count; + Console.WriteLine($"epoch: {epoch} | batch: {batch} / {batch_count} | accuracy: {accuracy:0.00}"); + } + } + } + } + + static double evaluate(IEnumerable<(Tensor, Tensor, Tensor, Tensor, Tensor)> test_data, RodNet model, Loss criterion) + { + model.eval(); + + double total_acc = 0.0; + long total_count = 0; + + using (var d = torch.NewDisposeScope()) + { + foreach (var (y0z0t, uv, h, fish_label, success) in test_data) + { + using (var predicted_labels = model.forward(fish_label, uv, y0z0t, h)) + { + var loss = criterion.forward(predicted_labels, success.to(ScalarType.Int64)); + + total_acc += (predicted_labels.argmax(1) == success).sum().to(torch.CPU).item(); + total_count += success.size(0); + } + } + + return total_acc / total_count; + } + } + #endregion + } + + internal class CSVReader : IDisposable + { + /// + /// + /// + /// 按长度分组,布尔值代表每组内此序号元素是否被读取。例如8个true2个false就是将近80%进入训练集 + /// + /// + public CSVReader(IEnumerable takeMask, string path, Device device) + { + this.takeMask = takeMask.ToArray(); + _path = path; + _device = device; + } + + private readonly bool[] takeMask; + private readonly string _path; + private readonly Device _device; + + + public IEnumerable Enumerate() + { + var all = File.ReadLines(_path).Skip(1); // 跳过首行列名 + int count = takeMask.Length; + int maskCount = all.Count() / count; + for (int i = 0; i < maskCount + 1; i++) + { + int lastGroupCount = (i == maskCount) ? (all.Count() % count) : count; + for (int j = 0; j < lastGroupCount; j++) + { + if (takeMask[j]) + { + yield return ParseLine(all.Skip(i * count + j).First()); + } + } + } + } + + public IEnumerable<(Tensor, Tensor, Tensor, Tensor, Tensor)> GetBatches(long batch_size) + { + // This data set fits in memory, so we will simply load it all and cache it between epochs. + + var inputs = new List(); + + if (_data == null) + { + + _data = new List<(Tensor, Tensor, Tensor, Tensor, Tensor)>(); + + var counter = 0; + var lines = Enumerate().ToList(); + var left = lines.Count; + + foreach (var line in lines) + { + + inputs.Add(line); + left -= 1; + + if (++counter == batch_size || left == 0) + { + _data.Add(Batchifier(inputs)); + inputs.Clear(); + counter = 0; + } + } + } + + return _data; + } + + private List<(Tensor, Tensor, Tensor, Tensor, Tensor)> _data; + private bool disposedValue; + + /// + /// 将csv中的数据进行初步转换 + /// + /// + /// y0z0t、uv、h、fish_label、success张量 + private (Tensor, Tensor, Tensor, Tensor, Tensor) Batchifier(IEnumerable input) + { + var y0List = new List(); + var z0List = new List(); + var tList = new List(); + var uList = new List(); + var vList = new List(); + var hList = new List(); + var labelList = new List(); + var successList = new List(); + + foreach (var line in input) + { + int fish_label = line.fish_label; + int success = line.success; + RodInput rodInput = new RodInput() + { + rod_x1 = line.rod_x1, + rod_x2 = line.rod_x2, + rod_y1 = line.rod_y1, + rod_y2 = line.rod_y2, + fish_x1 = line.fish_x1, + fish_x2 = line.fish_x2, + fish_y1 = line.fish_y1, + fish_y2 = line.fish_y2 + }; + var (y0, z0, t, u, v, h) = RodNet.GetRodStatePreProcess(rodInput); + + y0List.Add(y0); + z0List.Add(z0); + tList.Add(t); + uList.Add(u); + vList.Add(v); + hList.Add(h); + labelList.Add(fish_label); + successList.Add(success); + } + + Tensor y0Tensor = tensor(y0List, dtype: ScalarType.Float64).to(_device); + Tensor z0Tensor = tensor(z0List, dtype: ScalarType.Float64).to(_device); + Tensor tTensor = tensor(tList, dtype: ScalarType.Float64).to(_device); + Tensor uTensor = tensor(uList, dtype: ScalarType.Float64).to(_device); + Tensor vTensor = tensor(vList, dtype: ScalarType.Float64).to(_device); + Tensor hTensor = tensor(hList, dtype: ScalarType.Float64).to(_device); + Tensor fish_labelTensor = tensor(labelList, dtype: ScalarType.Int32).to(_device); + Tensor successTensor = tensor(successList, dtype: ScalarType.Int32).to(_device); + + return (torch.stack([y0Tensor, z0Tensor, tTensor], dim: 1), torch.stack([uTensor, vTensor], dim: 1), hTensor.unsqueeze(1), fish_labelTensor, successTensor); + } + + /// + /// csv的列定义默认为time,bite_time,rod_x1,rod_x2,rod_y1,rod_y2,fish_x1,fish_x2,fish_y1,fish_y2,fish_label,success + /// + /// + /// + public Data ParseLine(string line) + { + var columns = line.Split(",").ToArray(); + + return new Data(float.Parse(columns[2]), float.Parse(columns[3]), float.Parse(columns[4]), float.Parse(columns[5]), + float.Parse(columns[6]), float.Parse(columns[7]), float.Parse(columns[8]), float.Parse(columns[9]), + int.Parse(columns[10]), int.Parse(columns[11])); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing && _data != null) + { + foreach (var (y0z0t, uv, h, label, success) in _data) + { + y0z0t.Dispose(); + uv.Dispose(); + h.Dispose(); + label.Dispose(); + success.Dispose(); + } + } + + disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + + internal record Data(float rod_x1, float rod_x2, float rod_y1, float rod_y2, float fish_x1, float fish_x2, float fish_y1, float fish_y2, int fish_label, int success); +} diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.cs index d4546c83..6440c8e9 100644 --- a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.cs +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/AutoFishingTests/RodNetTests.cs @@ -1,17 +1,16 @@ using BetterGenshinImpact.GameTask.AutoFishing; -using BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using static BetterGenshinImpact.GameTask.AutoFishing.RodNet; +using TorchSharp; using static TorchSharp.torch; namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests { [Collection("Init Collection")] - public class RodNetTests + public partial class RodNetTests { public RodNetTests(TorchFixture torch) { @@ -42,9 +41,8 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests RodNet sut = new RodNet(); // - NetInput netInput = GeometryProcessing(rodInput) ?? throw new NullReferenceException(); - Tensor outputTensor = sut.ComputeScores_Torch(netInput); - double[] pred = ComputeScores(netInput); + Tensor outputTensor = sut.ComputeScores_Torch(rodInput); + double[] pred = RodNet.ComputeScores(rodInput); // Assert.Equal((float)pred[0], (float)outputTensor.data()[0]); // 对比时降低精度,差不多就行 diff --git a/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs new file mode 100644 index 00000000..0d599679 --- /dev/null +++ b/Test/BetterGenshinImpact.UnitTest/GameTaskTests/Model/GameUI/GridScreenTests.cs @@ -0,0 +1,39 @@ +using GameTask.Model.GameUI; +using OpenCvSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI +{ + public class GridScreenTests + { + [Fact] + /// + /// 测试判断前后两张图是否属于滚动,结果应正确 + /// + public void IsScrolling_ResultShouldBeRight() + { + // + using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\ArtifactGrid.png", flags: ImreadModes.Grayscale); + using Mat cropped = mat[new Rect(0, 0, mat.Width, mat.Height - 10)]; + using Mat black = new Mat(mat.Size(), mat.Type(), Scalar.Black); + using Mat scrolled = black.Clone(); // 一个向下平移了10像素的图 + using Mat pos = scrolled[new Rect(0, 10, mat.Width, mat.Height - 10)]; + cropped.CopyTo(pos); + + // + bool result1 = GridScreen.GridEnumerator.IsScrolling(mat, scrolled, out Point2d shift); + bool result2 = GridScreen.GridEnumerator.IsScrolling(mat, mat, out Point2d _); + bool result3 = GridScreen.GridEnumerator.IsScrolling(mat, black, out Point2d _); + + // + Assert.True(result1); + Assert.True(shift.Y <= 10 && shift.Y > 9.9); + Assert.False(result2); + Assert.False(result3); + } + } +}