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.1falseWinExenet8.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