Merge branch 'main' into d-v3

This commit is contained in:
辉鸭蛋
2025-06-25 01:08:08 +08:00
77 changed files with 5734 additions and 1052 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -11,7 +11,9 @@
<ui:ControlsDictionary />
<vio:ControlsDictionary />
<ResourceDictionary Source="/View/Controls/WpfUi/FaFontIconStyle.xaml" />
<ResourceDictionary Source="/View/Controls/Drawer/DrawerStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<FontFamily x:Key="TextThemeFontFamily">/Assets/Fonts/MiSans-Regular.ttf#MiSans</FontFamily>
<FontFamily x:Key="DigitalThemeFontFamily">/Assets/Fonts/deluge-led.ttf#Deluge LED</FontFamily>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />

View File

@@ -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. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>BetterGI</AssemblyName>
<Version>0.45.3-alpha.1</Version>
<Version>0.46.3-alpha.1</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
@@ -43,11 +43,13 @@
<ItemGroup>
<PackageReference Include="AvalonEdit" Version="6.3.1.120" />
<PackageReference Include="BehaviourTree" Version="1.0.73" />
<PackageReference Include="BetterGI.VCRuntime" Version="14.44.35208" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="DeviceId" Version="6.9.0" />
<PackageReference Include="DeviceId.Windows" Version="6.9.0" />
<PackageReference Include="DeviceId.Windows.Wmi" Version="6.9.0" />
<PackageReference Include="Emoji.Wpf" Version="0.3.4" />
<PackageReference Include="LibGit2Sharp" Version="0.31.0" />
<PackageReference Include="Meziantou.Framework.Win32.CredentialManager" Version="1.7.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />

View File

@@ -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;
// }
}

View File

@@ -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;
/// <summary>
/// 初始化模板匹配器
/// </summary>
/// <param name="source">源图像</param>
/// <param name="templateSize">模板尺寸</param>
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();
}
/// <summary>
/// 执行模板匹配
/// </summary>
/// <param name="maskedTemplates">模板图像</param>
/// <param name="maskF">遮罩图像</param>
/// <returns>最佳匹配位置</returns> (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);
}
/// <summary>
///
/// </summary>
/// <param name="template">模板图</param>
/// <param name="mask">遮罩, 类型为 8UC1, 且尺寸与 template 相同</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,29 @@
namespace BetterGenshinImpact.Core.Recognition.OpenCv.TemplateMatch;
public static class MiniMapMatchConfig
{
/// <summary>
/// 原始小地图尺寸
/// </summary>
public const int OriginalSize = 156;
/// <summary>
/// 粗匹配时的地图尺寸
/// </summary>
public const int RoughSize = 52;
/// <summary>
/// 精确匹配时的地图尺寸
/// </summary>
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;
}

View File

@@ -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<double>(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));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
/// <summary>
/// 构造模板匹配归一化器
/// </summary>
/// <param name="mode">模板匹配类型</param>
/// <param name="template">模板图</param>
/// <param name="mask">遮罩</param>
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();
}
/// <summary>
/// 更新 value
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
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<Mat, Mat?, (double, double)> 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<double>(0, 0);
return (bestVal, -bestVal);
}
}

View File

@@ -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<ScriptRepoUpdater>
// 仓储临时目录 用于下载与解压
public static readonly string ReposTempPath = Path.Combine(ReposPath, "Temp");
// 中央仓库信息地址
public static readonly List<string> 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<string> 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<string, string> PathMapper = new Dictionary<string, string>
{
@@ -56,103 +60,212 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
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<ScriptRepoUpdater>
// 获取文件名
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<ScriptRepoUpdater>
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<ScriptRepoUpdater>
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<ScriptRepoUpdater>
}
// 使用路径分隔符分割路径
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<ScriptRepoUpdater>
_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<ScriptRepoUpdater>
}
}
public void OpenScriptRepoWindow()
{
var scriptRepoWindow = new ScriptRepoWindow { Owner = Application.Current.MainWindow };
scriptRepoWindow.ShowDialog();
}
/// <summary>
/// 处理带有 icon.ico 和 desktop.ini 的文件夹
/// </summary>

View File

@@ -20,15 +20,6 @@ public class RepoWebBridge
{
public async Task<string> GetRepoJson()
{
try
{
await ScriptRepoUpdater.Instance.UpdateCenterRepo();
}
catch (Exception e)
{
Toast.Warning($"更新仓库信息失败:{e.Message}");
}
try
{
if (!Directory.Exists(ScriptRepoUpdater.CenterRepoPath))

View File

@@ -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<string> checkedArtifactAffixesQueue = new Queue<string>();
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<Rect> 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<Rect> 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<Rect> 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<Rect> 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)));

View File

@@ -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<string> _resinPriorityList =
[
"浓缩树脂",
"原粹树脂"
];
// 使用原粹树脂刷取副本次数
[ObservableProperty]
private int _originalResinUseCount = 0;
//使用浓缩树脂刷取副本次数
[ObservableProperty]
private int _condensedResinUseCount = 0;
// 使用须臾树脂刷取副本次数
[ObservableProperty]
private int _transientResinUseCount = 0;
// 使用脆弱树脂刷取副本次数
[ObservableProperty]
private int _fragileResinUseCount = 0;
}

View File

@@ -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<string> 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;
}
}

View File

@@ -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<ResinUseRecord> _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<AutoDomainTask> stringLocalizer =
App.GetService<IStringLocalizer<AutoDomainTask>>() ?? 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
/// <summary>
/// 领取奖励
/// </summary>
/// <param name="recognizeResin">是否识别树脂</param>
/// <param name="isLastTurn">是否最后一轮</param>
private bool GettingTreasure(bool recognizeResin, bool isLastTurn)
private async Task<bool> 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("未检测到秘境结束,可能是背包物品已满。");
}
/// <summary>
/// 获取剩余树脂状态
/// </summary>
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<Region> 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 HHmmssffff")}.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);
/// <summary>
/// 判断两个区域在垂直方向上是否有重叠
/// </summary>
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()

View File

@@ -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
{
/// <summary>
/// 原粹树脂1自回体
/// </summary>
public int OriginalResinCount { get; set; } = 0;
/// <summary>
/// 脆弱树脂60
/// </summary>
public int FragileResinCount { get; set; } = 0;
/// <summary>
/// 浓缩树脂40
/// </summary>
public int CondensedResinCount { get; set; } = 0;
/// <summary>
/// 须臾树脂60壶内购买
/// </summary>
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);
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
namespace BetterGenshinImpact.GameTask.AutoDomain.Model;
/// <summary>
/// 树脂使用记录
/// 用于自动秘境指定树脂刷取次数时候,计算剩余刷取次数
/// </summary>
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<ResinUseRecord> BuildFromDomainParam(AutoDomainParam taskParam)
{
List<ResinUseRecord> 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -26,8 +26,11 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
// 树脂状态
public RecognitionObject CondensedResinCountRa;
public RecognitionObject FragileResinCountRa;
// 自动秘境
// public RecognitionObject LockIconRa; // 锁定辅助图标
public RecognitionObject CondensedResinTopIconRa;
public RecognitionObject OriginalResinTopIconRa;
public Dictionary<string, string> AvatarCostumeMap;
@@ -245,5 +248,31 @@ public class AutoFightAssets : BaseAssets<AutoFightAssets>
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();
}
}

View File

@@ -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"
}
]

View File

@@ -96,6 +96,11 @@ public partial class AutoFightConfig : ObservableObject
[ObservableProperty]
private int _pickDropsAfterFightSeconds = 15;
/// <summary>
/// 拾取战斗人次阈值,当战斗人次小于一定次数就结束战斗情况下不触发拾取掉落物和万叶拾取后拾取只有不小于2时才生效。
/// </summary>
[ObservableProperty]
private int? _battleThresholdForLoot;
/// <summary>
/// 战斗结束后,如果存在枫原万叶,则使用该角色捡材料
/// </summary>

View File

@@ -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;

View File

@@ -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("距最近一次万叶出招,时间过短,跳过此次万叶拾取!");
}
}
//切换过队伍的,需要再切回来

View File

@@ -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<BigFishType> Values

View File

@@ -1,4 +1,6 @@
namespace BetterGenshinImpact.GameTask.AutoFishing;
using static TorchSharp.torch;
namespace BetterGenshinImpact.GameTask.AutoFishing;
public record RodInput
{

View File

@@ -37,8 +37,14 @@ namespace BetterGenshinImpact.GameTask.AutoFishing;
/// tmd 今天我意识到 XXXX可不就是XXXX
///
/// 哦 到这一步以后剩下的就很弱智了 远了挪近一点 近了挪远一点 调调参差不多得了
///
/// *后来又新增了一些访谈内容:
///
/// 额 总之就是要求不能把不咬钩的识别成咬钩的 但是咬钩的可以识别成不咬钩的
///
/// 然后就可视化一下onehot在不同距离的结果 加一个offset使得模型输出的结果在保证可以predict距离正好的结果的同时距离范围尽可能小
/// </summary>
public class RodNet : Module<Tensor, Tensor>
public class RodNet : Module<Tensor, Tensor, Tensor, Tensor, Tensor>
{
const double alpha = 1734.34 / 2.5;
// fitted parameters
@@ -75,23 +81,21 @@ public class RodNet : Module<Tensor, Tensor>
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<Tensor, Tensor> 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<Tensor, Tensor>)>
{
($"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<Tensor, Tensor>
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<Tensor, Tensor>
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<long>();
}
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)
/// <summary>
/// 使用时直接赋值已知权重
/// </summary>
public void SetWeightsManually()
{
return layers.forward(input);
}
}
public class RodLayer1 : Module<Tensor, Tensor>
{
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<double>()));
var embed2 = embedding2.forward(fish_label);
//Console.WriteLine(String.Join(",", embed2.data<double>()));
Tensor x_offset = tensor(fishLabel.data<int>().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);
/// <summary>
/// 根据rod和fish的坐标计算y0z0t、uv、h
/// </summary>
/// <param name="input"></param>
/// <returns>y0, z0, t, u, v, h</returns>
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);
}
}

View File

@@ -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": "其他武器",

View File

@@ -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
{

View File

@@ -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);
}
}
}

View File

@@ -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<RecognitionObject>();
@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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<BaseMapLayerByTemplateMatch> LoadLayers(SceneBaseMapByTemplateMatch sceneBaseMap)
{
var layers = new List<BaseMapLayerByTemplateMatch>();
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<List<BaseMapLayerByTemplateMatch>>(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));
}
}

View File

@@ -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)
{

View File

@@ -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<BaseMapLayerByTemplateMatch> 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<SceneBaseMapByTemplateMatch>(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
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
/// </summary>
public static class NewRetry
{
/// <summary>
/// 重试指定操作,若抛出 RetryException 则在指定间隔后重试,最多尝试 maxAttemptCount 次。
/// </summary>
/// <param name="action">要执行的操作</param>
/// <param name="retryInterval">重试间隔</param>
/// <param name="maxAttemptCount">最大尝试次数</param>
public static void Do(Action action, TimeSpan retryInterval, int maxAttemptCount = 3)
{
_ = Do<object?>(() =>
@@ -21,6 +29,14 @@ public static class NewRetry
}, retryInterval, maxAttemptCount);
}
/// <summary>
/// 重试指定操作,若抛出 RetryException 则在指定间隔后重试,最多尝试 maxAttemptCount 次,返回操作结果。
/// </summary>
/// <typeparam name="T">返回值类型</typeparam>
/// <param name="action">要执行的操作</param>
/// <param name="retryInterval">重试间隔</param>
/// <param name="maxAttemptCount">最大尝试次数</param>
/// <returns>操作结果</returns>
public static T Do<T>(Func<T> action, TimeSpan retryInterval, int maxAttemptCount = 3)
{
List<System.Exception> exceptions = [];
@@ -49,6 +65,14 @@ public static class NewRetry
throw new AggregateException(exceptions);
}
/// <summary>
/// 重试执行 action直到返回 true 或达到最大重试次数。
/// </summary>
/// <param name="action">判断条件</param>
/// <param name="ct">取消令牌</param>
/// <param name="retryTimes">最大重试次数</param>
/// <param name="delayMs">每次重试间隔(毫秒)</param>
/// <returns>是否成功</returns>
public static async Task<bool> WaitForAction(Func<bool> action, CancellationToken ct, int retryTimes = 10, int delayMs = 1000)
{
for (var i = 0; i < retryTimes; i++)
@@ -61,4 +85,84 @@ public static class NewRetry
}
return false;
}
/// <summary>
/// 重试直到某个元素出现,可执行键盘或鼠标操作。
/// </summary>
/// <param name="recognitionObject">要识别的目标对象</param>
/// <param name="retryAction">每次重试时执行的操作</param>
/// <param name="ct">取消令牌</param>
/// <param name="maxAttemptCount">最大尝试次数</param>
/// <param name="retryInterval">重试间隔(毫秒)</param>
/// <returns>是否成功找到元素</returns>
public static async Task<bool> WaitForElementAppear(
RecognitionObject recognitionObject,
Action retryAction,
CancellationToken ct,
int maxAttemptCount = 10,
int retryInterval = 1000
)
{
for (int i = 0; i < maxAttemptCount; i++)
{
if (ct.IsCancellationRequested) return false;
// 执行重试操作(如按键)
retryAction?.Invoke();
// 等待指定时间
await TaskControl.Delay(retryInterval, ct);
// 截图并查找元素
using var screen = CaptureToRectArea();
using var result = screen.Find(recognitionObject);
// 元素已出现
if (!result.IsEmpty())
{
return true;
}
}
return false;
}
/// <summary>
/// 重试直到某个元素消失,可执行键盘或鼠标操作。
/// </summary>
/// <param name="recognitionObject">要识别的目标对象</param>
/// <param name="retryAction">每次重试时执行的操作</param>
/// <param name="ct">取消令牌</param>
/// <param name="maxAttemptCount">最大尝试次数</param>
/// <param name="retryInterval">重试间隔(毫秒)</param>
/// <returns>是否成功等待元素消失</returns>
public static async Task<bool> WaitForElementDisappear(
RecognitionObject recognitionObject,
Action retryAction,
CancellationToken ct,
int maxAttemptCount = 10,
int retryInterval = 1000)
{
for (int i = 0; i < maxAttemptCount; i++)
{
if (ct.IsCancellationRequested) return false;
// 执行重试操作(如按键)
retryAction?.Invoke();
// 等待指定时间
await TaskControl.Delay(retryInterval, ct);
// 截图并查找元素
using var screen = CaptureToRectArea();
using var result = screen.Find(recognitionObject);
// 元素已消失
if (result.IsEmpty())
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,232 @@
using BetterGenshinImpact.Core.Simulator;
using BetterGenshinImpact.GameTask.Common;
using BetterGenshinImpact.GameTask.Model.Area;
using Fischless.WindowsInput;
using Microsoft.Extensions.Logging;
using OpenCvSharp;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace GameTask.Model.GameUI
{
public class GridScreen : IAsyncEnumerable<ImageRegion>
{
private readonly Rect gridRoi;
private readonly CancellationToken ct;
private readonly ILogger logger;
private readonly InputSimulator input = Simulation.SendInput;
private readonly int s1Round;
private readonly int roundMilliseconds;
private readonly int s2Round;
private readonly double s3Scale;
/// <summary>
/// 对Gird类型界面的操作封装类
/// 直接对此类对象进行遍历即可获取所有项
/// 每次的截图是上次滚动后的,如果实时性要求高,应每次迭代自行截图
/// 在末页可能重复返回GridItem须自行处理
/// </summary>
/// <param name="gridRoi">Grid所在位置</param>
/// <param name="logger"></param>
/// <param name="ct"></param>
public GridScreen(Rect gridRoi, int s1Round, int roundMilliseconds, int s2Round, double s3Scale, ILogger logger, CancellationToken ct)
{
this.gridRoi = gridRoi;
this.ct = ct;
this.logger = logger;
this.s1Round = s1Round;
this.roundMilliseconds = roundMilliseconds;
this.s2Round = s2Round;
this.s3Scale = s3Scale;
}
public IAsyncEnumerator<ImageRegion> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return new GridEnumerator(this.gridRoi, this.s1Round, this.roundMilliseconds, this.s2Round, this.s3Scale, this.logger, this.input, this.ct);
}
public class GridEnumerator : IAsyncEnumerator<ImageRegion>
{
private readonly Rect roi;
private readonly CancellationToken ct;
private readonly ILogger logger;
private readonly InputSimulator input = Simulation.SendInput;
private readonly int s1Round;
private readonly int roundMilliseconds;
private readonly int s2Round;
private readonly double s3Scale;
private record Page(ImageRegion ImageRegion, Stack<Rect> Rects);
private Page? currentPage;
private ImageRegion current;
ImageRegion IAsyncEnumerator<ImageRegion>.Current => current;
/// <summary>
/// 滚动操作枚举器
/// </summary>
/// <param name="roi"></param>
/// <param name="s1Round">测试是否能滚动时发出的滚动命令次数</param>
/// <param name="roundMilliseconds">滚动命令间隔毫秒</param>
/// <param name="s2Round">滚过一整页时发出的滚动命令次数</param>
/// <param name="s3Scale">微调滚动时控制首行距离上边界的参数</param>
/// <param name="logger"></param>
/// <param name="input"></param>
/// <param name="ct"></param>
public GridEnumerator(Rect roi, int s1Round, int roundMilliseconds, int s2Round, double s3Scale, ILogger logger, InputSimulator input, CancellationToken ct)
{
this.roi = roi;
this.ct = ct;
this.logger = logger;
this.input = input;
this.s1Round = s1Round;
this.roundMilliseconds = roundMilliseconds;
this.s2Round = s2Round;
this.s3Scale = s3Scale;
}
public async Task<bool> TryVerticalScollDown()
{
using var ra = TaskControl.CaptureToRectArea();
using ImageRegion prevGrid = ra.DeriveCrop(roi);
for (int i = 0; i < this.s1Round; i++)
{
this.input.Mouse.VerticalScroll(-2);
await TaskControl.Delay(this.roundMilliseconds, this.ct);
}
await TaskControl.Delay(300, this.ct);
using var ra2 = TaskControl.CaptureToRectArea();
using ImageRegion scrolledGrid = ra2.DeriveCrop(this.roi);
bool isScrolling = IsScrolling(prevGrid.CacheGreyMat, scrolledGrid.CacheGreyMat, out Point2d shift, logger: this.logger);
return isScrolling;
}
/// <summary>
/// 判断是否还能继续滚动,如果到底了则只能滚动一丝并很快地回弹
/// </summary>
/// <param name="prevGray">先前的灰度图</param>
/// <param name="nextGray">尝试滚动并等待可能的回弹后的灰度图</param>
/// <param name="shift">估计的位移</param>
/// <param name="lowerThreshold">低于下限则可能不存在平移</param>
/// <param name="upperThreshold">上限用于抵消微小的其他差异</param>
/// <param name="logger"></param>
/// <returns></returns>
public static bool IsScrolling(Mat prevGray, Mat nextGray, out Point2d shift, double lowerThreshold = 0.5, double upperThreshold = 0.95, ILogger? logger = null)
{
using Mat prev = new Mat();
prevGray.ConvertTo(prev, MatType.CV_32FC1);
using Mat next = new Mat();
nextGray.ConvertTo(next, MatType.CV_32FC1);
using Mat window = new Mat();
shift = Cv2.PhaseCorrelate(prev, next, window, out double response); // 相位相关性
logger?.LogInformation($"response={response:F3}, shift=({shift.X:F2}, {shift.Y:F2})");
return response > lowerThreshold && response < upperThreshold;
}
public static IEnumerable<Rect> GetGridItems(Mat src)
{
using Mat grey = src.CvtColor(ColorConversionCodes.BGR2GRAY);
using Mat canny = grey.Canny(20, 40);
Cv2.FindContours(canny, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple, null);
IEnumerable<Rect> boxes = contours.Where(c => Cv2.MinAreaRect(c).Angle % 90 <= 1) // 剔除倾斜
.Select(Cv2.BoundingRect).Where(r =>
{
if (r.Height == 0)
{
return false;
}
return Math.Abs((float)r.Width / r.Height - 0.8) < 0.05; // 按形状筛选
}).ToList();
//src.DrawContours(contours, -1, Scalar.Red);
int biggestRectHeight = boxes.Max(b => b.Height);
boxes = boxes.Where(b => (float)b.Height / biggestRectHeight > 0.88); // 剔除太小的
return boxes.ToArray();
}
public async ValueTask<bool> MoveNextAsync()
{
if (this.currentPage == null || this.currentPage.Rects.Count < 1)
{
if (this.currentPage != null)
{
//BetterGenshinImpact.View.Drawable.VisionContext.Instance().DrawContent.ClearAll();
using var ra4 = TaskControl.CaptureToRectArea();
ra4.MoveTo(this.roi.X + this.roi.Width / 2, this.roi.Y + this.roi.Height / 2);
await TaskControl.Delay(300, ct);
bool canScoll = await TryVerticalScollDown();
if (canScoll)
{
for (int i = 0; i < this.s2Round; i++) // 再滚动差不多(最多行数-1
{
input.Mouse.VerticalScroll(-2);
await TaskControl.Delay(this.roundMilliseconds, ct);
}
DateTimeOffset rollingEndTime = DateTime.Now.AddSeconds(2);
while (DateTime.Now < rollingEndTime)
{
await TaskControl.Delay(60, ct);
using var ra2 = TaskControl.CaptureToRectArea();
using ImageRegion grid2 = ra2.DeriveCrop(this.roi);
IEnumerable<Rect> gridItems2 = GetGridItems(grid2.SrcMat);
if (gridItems2.Min(i => i.Y) > (ra2.Width * this.s3Scale)) // 最后精细滚动,保证完整地显示最多行
{
input.Mouse.VerticalScroll(-1);
}
else
{
break;
}
}
using var ra3 = TaskControl.CaptureToRectArea();
using ImageRegion grid3 = ra3.DeriveCrop(this.roi);
grid3.MoveTo(grid3.Width, grid3.Height);
await TaskControl.Delay(300, ct);
}
else
{
await TaskControl.Delay(300, ct);
this.logger.LogInformation("滚动到底部了");
return false;
}
}
using var ra = TaskControl.CaptureToRectArea();
var imageRegion = ra.DeriveCrop(this.roi);
IEnumerable<Rect> gridItems = GetGridItems(imageRegion.SrcMat);
this.currentPage = new Page(imageRegion, new Stack<Rect>(gridItems));
//foreach (Rect item in gridItems)
//{
// imageRegion.DrawRect(item, item.GetHashCode().ToString(), new System.Drawing.Pen(System.Drawing.Color.Blue));
//}
}
this.current = this.currentPage.ImageRegion.DeriveCrop(this.currentPage.Rects.Pop());
return true;
}
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
}
}
}

View File

@@ -88,21 +88,21 @@ public partial class StringUtils
return int.TryParse(text, out int result) ? result : defaultValue;
}
public static int TryExtractPositiveInt(string text)
public static int TryExtractPositiveInt(string text, int defaultValue = -1)
{
if (string.IsNullOrEmpty(text))
{
return -1;
return defaultValue;
}
try
{
text = RegexHelper.ExcludeNumberRegex().Replace(text, "");
return int.Parse(text);
return TryParseInt(text, defaultValue);
}
catch
{
return -1;
return defaultValue;
}
}

View File

@@ -1,5 +1,6 @@
using System;
using Windows.System;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.View.Windows;
using Meziantou.Framework.Win32;
using Wpf.Ui.Violeta.Controls;
@@ -31,7 +32,7 @@ public static class MirrorChyanHelper
LeftButtonText = "获取CDK",
LeftButtonClick = (sender, args) =>
{
Launcher.LaunchUriAsync(new Uri("https://mirrorchyan.com/zh/get-start"));
OpenMirrorChyanWebsite();
}
}
);
@@ -67,7 +68,7 @@ public static class MirrorChyanHelper
LeftButtonText = "获取CDK",
LeftButtonClick = (sender, args) =>
{
Launcher.LaunchUriAsync(new Uri("https://mirrorchyan.com/zh/get-start"));
OpenMirrorChyanWebsite();
}
}
);
@@ -90,4 +91,10 @@ public static class MirrorChyanHelper
{
CredentialManagerHelper.DeleteCredential(MirrorChyanCdkAppName);
}
private static void OpenMirrorChyanWebsite()
{
Launcher.LaunchUriAsync(new Uri($"https://mirrorchyan.com/zh/get-start?source=bgi-{Global.Version}"));
}
}

View File

@@ -0,0 +1,410 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.ComponentModel;
namespace BetterGenshinImpact.View.Controls.Drawer;
public class CustomDrawer : ContentControl
{
#region
public static readonly DependencyProperty IsOpenProperty =
DependencyProperty.Register(nameof(IsOpen), typeof(bool), typeof(CustomDrawer),
new PropertyMetadata(false, OnIsOpenChanged));
public static readonly DependencyProperty DrawerPositionProperty =
DependencyProperty.Register(nameof(DrawerPosition), typeof(DrawerPosition), typeof(CustomDrawer),
new PropertyMetadata(DrawerPosition.Right, OnDrawerPositionChanged));
public static readonly DependencyProperty OpenWidthProperty =
DependencyProperty.Register(nameof(OpenWidth), typeof(double), typeof(CustomDrawer),
new PropertyMetadata(400.0));
public static readonly DependencyProperty OpenHeightProperty =
DependencyProperty.Register(nameof(OpenHeight), typeof(double), typeof(CustomDrawer),
new PropertyMetadata(300.0));
public static readonly DependencyProperty AnimationDurationProperty =
DependencyProperty.Register(nameof(AnimationDuration), typeof(TimeSpan), typeof(CustomDrawer),
new PropertyMetadata(TimeSpan.FromMilliseconds(200)));
public static readonly DependencyProperty BackgroundOpacityProperty =
DependencyProperty.Register(nameof(BackgroundOpacity), typeof(double), typeof(CustomDrawer),
new PropertyMetadata(0.6));
public static readonly DependencyProperty DrawerBackgroundProperty =
DependencyProperty.Register(nameof(DrawerBackground), typeof(Brush), typeof(CustomDrawer),
new PropertyMetadata(Brushes.Black));
#endregion
#region
/// <summary>
/// 抽屉打开后触发的事件
/// </summary>
public event EventHandler Opened;
/// <summary>
/// 抽屉关闭前触发的事件,可以取消关闭操作
/// </summary>
public event CancelEventHandler Closing;
/// <summary>
/// 引发 Opened 事件
/// </summary>
protected virtual void OnOpened()
{
Opened?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// 引发 Closing 事件
/// </summary>
/// <returns>如果取消关闭,则返回 true否则返回 false</returns>
protected virtual bool OnClosing()
{
if (Closing != null)
{
CancelEventArgs args = new CancelEventArgs();
Closing(this, args);
return args.Cancel;
}
return false;
}
#endregion
#region
public bool IsOpen
{
get => (bool)GetValue(IsOpenProperty);
set => SetValue(IsOpenProperty, value);
}
public DrawerPosition DrawerPosition
{
get => (DrawerPosition)GetValue(DrawerPositionProperty);
set => SetValue(DrawerPositionProperty, value);
}
public double OpenWidth
{
get => (double)GetValue(OpenWidthProperty);
set => SetValue(OpenWidthProperty, value);
}
public double OpenHeight
{
get => (double)GetValue(OpenHeightProperty);
set => SetValue(OpenHeightProperty, value);
}
public TimeSpan AnimationDuration
{
get => (TimeSpan)GetValue(AnimationDurationProperty);
set => SetValue(AnimationDurationProperty, value);
}
public double BackgroundOpacity
{
get => (double)GetValue(BackgroundOpacityProperty);
set => SetValue(BackgroundOpacityProperty, value);
}
public Brush DrawerBackground
{
get => (Brush)GetValue(DrawerBackgroundProperty);
set => SetValue(DrawerBackgroundProperty, value);
}
#endregion
private Border _backgroundOverlay;
private Border _drawerContainer;
private Grid _mainGrid;
static CustomDrawer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomDrawer),
new FrameworkPropertyMetadata(typeof(CustomDrawer)));
}
public CustomDrawer()
{
this.Loaded += CustomDrawer_Loaded;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_mainGrid = GetTemplateChild("PART_MainGrid") as Grid;
_backgroundOverlay = GetTemplateChild("PART_BackgroundOverlay") as Border;
_drawerContainer = GetTemplateChild("PART_DrawerContainer") as Border;
if (_backgroundOverlay != null)
{
_backgroundOverlay.MouseDown += BackgroundOverlay_MouseDown;
}
UpdateDrawerPosition();
UpdateOpenState(false);
}
private void CustomDrawer_Loaded(object sender, RoutedEventArgs e)
{
UpdateOpenState(false);
}
private void BackgroundOverlay_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
CloseDrawer();
}
private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomDrawer drawer)
{
bool newValue = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
// 如果是从打开到关闭状态,需要触发关闭前事件
if (oldValue && !newValue)
{
bool cancel = drawer.OnClosing();
if (cancel)
{
// 如果取消关闭,则恢复 IsOpen 为 true
drawer.IsOpen = true;
return;
}
}
// 更新抽屉状态(动画)
drawer.UpdateOpenState(true);
}
}
private static void OnDrawerPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is CustomDrawer drawer)
{
drawer.UpdateDrawerPosition();
drawer.UpdateOpenState(false);
}
}
private void UpdateDrawerPosition()
{
if (_drawerContainer == null) return;
switch (DrawerPosition)
{
case DrawerPosition.Left:
_drawerContainer.HorizontalAlignment = HorizontalAlignment.Left;
_drawerContainer.VerticalAlignment = VerticalAlignment.Stretch;
_drawerContainer.Width = OpenWidth;
_drawerContainer.Height = double.NaN;
break;
case DrawerPosition.Right:
_drawerContainer.HorizontalAlignment = HorizontalAlignment.Right;
_drawerContainer.VerticalAlignment = VerticalAlignment.Stretch;
_drawerContainer.Width = OpenWidth;
_drawerContainer.Height = double.NaN;
break;
case DrawerPosition.Top:
_drawerContainer.HorizontalAlignment = HorizontalAlignment.Stretch;
_drawerContainer.VerticalAlignment = VerticalAlignment.Top;
_drawerContainer.Width = double.NaN;
_drawerContainer.Height = OpenHeight;
break;
case DrawerPosition.Bottom:
_drawerContainer.HorizontalAlignment = HorizontalAlignment.Stretch;
_drawerContainer.VerticalAlignment = VerticalAlignment.Bottom;
_drawerContainer.Width = double.NaN;
_drawerContainer.Height = OpenHeight;
break;
}
}
private void UpdateOpenState(bool animate)
{
if (_drawerContainer == null || _backgroundOverlay == null) return;
_backgroundOverlay.IsHitTestVisible = IsOpen;
_drawerContainer.Opacity = 1;
if (IsOpen)
{
Visibility = Visibility.Visible;
}
// 每次更新状态时重新应用宽高
switch (DrawerPosition)
{
case DrawerPosition.Left:
case DrawerPosition.Right:
_drawerContainer.Width = OpenWidth;
_drawerContainer.Height = double.NaN;
break;
case DrawerPosition.Top:
case DrawerPosition.Bottom:
_drawerContainer.Width = double.NaN;
_drawerContainer.Height = OpenHeight;
break;
}
if (animate)
{
// 动画背景遮罩
DoubleAnimation backgroundAnimation = new DoubleAnimation
{
To = IsOpen ? BackgroundOpacity : 0,
Duration = AnimationDuration
};
_backgroundOverlay.BeginAnimation(OpacityProperty, backgroundAnimation);
// 确保RenderTransform已设置
if (_drawerContainer.RenderTransform == null || !(_drawerContainer.RenderTransform is TranslateTransform))
{
_drawerContainer.RenderTransform = new TranslateTransform();
}
TranslateTransform transform = (TranslateTransform)_drawerContainer.RenderTransform;
// 动画抽屉
DoubleAnimation drawerAnimation = new DoubleAnimation
{
Duration = AnimationDuration,
// 弹性动画效果
// EasingFunction = IsOpen
// ? new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.3 }
// : new ExponentialEase { EasingMode = EasingMode.EaseIn, Exponent = 6 }
};
// 如果是打开操作在动画完成时触发Opened事件
if (IsOpen)
{
drawerAnimation.Completed += (s, e) => OnOpened();
}
switch (DrawerPosition)
{
case DrawerPosition.Left:
// 打开时,先设置初始位置
if (IsOpen)
{
transform.X = -OpenWidth;
}
drawerAnimation.To = IsOpen ? 0 : -OpenWidth;
transform.BeginAnimation(TranslateTransform.XProperty, drawerAnimation);
break;
case DrawerPosition.Right:
// 打开时,先设置初始位置
if (IsOpen)
{
transform.X = OpenWidth;
}
drawerAnimation.To = IsOpen ? 0 : OpenWidth;
transform.BeginAnimation(TranslateTransform.XProperty, drawerAnimation);
break;
case DrawerPosition.Top:
// 打开时,先设置初始位置
if (IsOpen)
{
transform.Y = -OpenHeight;
}
drawerAnimation.To = IsOpen ? 0 : -OpenHeight;
transform.BeginAnimation(TranslateTransform.YProperty, drawerAnimation);
break;
case DrawerPosition.Bottom:
// 打开时,先设置初始位置
if (IsOpen)
{
transform.Y = OpenHeight;
}
drawerAnimation.To = IsOpen ? 0 : OpenHeight;
transform.BeginAnimation(TranslateTransform.YProperty, drawerAnimation);
break;
}
if (!IsOpen)
{
drawerAnimation.Completed += (s, e) =>
{
if (!IsOpen)
{
Visibility = Visibility.Collapsed;
}
};
}
}
else
{
// 无动画直接设置
_backgroundOverlay.Opacity = IsOpen ? BackgroundOpacity : 0;
TranslateTransform transform = new TranslateTransform();
_drawerContainer.RenderTransform = transform;
switch (DrawerPosition)
{
case DrawerPosition.Left:
transform.X = IsOpen ? 0 : -OpenWidth;
break;
case DrawerPosition.Right:
transform.X = IsOpen ? 0 : OpenWidth;
break;
case DrawerPosition.Top:
transform.Y = IsOpen ? 0 : -OpenHeight;
break;
case DrawerPosition.Bottom:
transform.Y = IsOpen ? 0 : OpenHeight;
break;
}
Visibility = IsOpen ? Visibility.Visible : Visibility.Collapsed;
// 如果是无动画模式且正在打开立即触发Opened事件
if (IsOpen)
{
OnOpened();
}
}
}
/// <summary>
/// 关闭抽屉的方法,会触发 Closing 事件并可能被取消
/// </summary>
/// <returns>如果成功关闭返回 true如果被取消返回 false</returns>
public bool CloseDrawer()
{
if (!IsOpen)
return true;
if (OnClosing())
return false;
IsOpen = false;
return true;
}
}
public enum DrawerPosition
{
Left,
Right,
Top,
Bottom
}

View File

@@ -0,0 +1,27 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:drawer="clr-namespace:BetterGenshinImpact.View.Controls.Drawer">
<Style TargetType="{x:Type drawer:CustomDrawer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type drawer:CustomDrawer}">
<Grid x:Name="PART_MainGrid">
<Border x:Name="PART_BackgroundOverlay"
Background="Black"
Opacity="0"/>
<Border x:Name="PART_DrawerContainer"
Background="{TemplateBinding DrawerBackground}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="0"
Effect="{DynamicResource ControlElevationBorderShadow}">
<ContentPresenter Margin="{TemplateBinding Padding}"/>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,71 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace BetterGenshinImpact.View.Controls.Drawer;
public partial class DrawerViewModel : ObservableObject
{
[ObservableProperty]
private bool _isDrawerOpen;
[ObservableProperty]
private object? _drawerContent;
[ObservableProperty]
private DrawerPosition _drawerPosition = DrawerPosition.Right;
[ObservableProperty]
private double _drawerWidth = 400;
[ObservableProperty]
private double _drawerHeight = 300;
[ObservableProperty]
private RelayCommand _onDrawerOpenedCommand;
[ObservableProperty]
private RelayCommand<CancelEventArgs> _onDrawerClosingCommand;
public void setDrawerOpenedAction(Action action)
{
OnDrawerOpenedCommand = new RelayCommand(action!);
}
public void SetDrawerClosingAction(Action<CancelEventArgs> action)
{
OnDrawerClosingCommand = new RelayCommand<CancelEventArgs>(action!);
}
[RelayCommand]
public void OpenDrawer(object content)
{
DrawerContent = content;
IsDrawerOpen = true;
}
[RelayCommand]
public void CloseDrawer()
{
IsDrawerOpen = false;
}
[RelayCommand]
public void ToggleDrawer(object? content = null)
{
if (IsDrawerOpen)
{
CloseDrawer();
}
else
{
if (content != null)
{
DrawerContent = content;
}
IsDrawerOpen = true;
}
}
}

View File

@@ -4,11 +4,13 @@ using Microsoft.Web.WebView2.Wpf;
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.AccessControl;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using BetterGenshinImpact.Helpers;
namespace BetterGenshinImpact.View.Controls.Webview;
@@ -23,6 +25,8 @@ public class WebpagePanel : UserControl
public string? DownloadFolderPath { get; set; }
public Action? OnWebViewInitializedAction { get; set; }
public Action<CoreWebView2NavigationCompletedEventArgs>? OnNavigationCompletedAction { get; set; }
public WebpagePanel()
{
@@ -45,10 +49,37 @@ public class WebpagePanel : UserControl
};
_webView.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
_webView.NavigationStarting += NavigationStarting_CancelNavigation;
_webView.NavigationCompleted += WebView_NavigationCompleted;
Content = _webView;
}
}
// public WebpagePanel(WebView2 webView2)
// {
// if (!IsWebView2Available())
// {
// Content = CreateDownloadButton();
// }
// else
// {
// EnsureWebView2DataFolder();
// _webView = webView2;
// webView2.CreationProperties = new CoreWebView2CreationProperties
// {
// UserDataFolder = Path.Combine(new FileInfo(Environment.ProcessPath!).DirectoryName!, @"WebView2Data\\"),
// };
// webView2.CoreWebView2InitializationCompleted += WebView_CoreWebView2InitializationCompleted;
// webView2.NavigationStarting += NavigationStarting_CancelNavigation;
// Content = webView2;
// }
// }
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
// 调用外部设置的导航完成 Action
OnNavigationCompletedAction?.Invoke(e);
}
private void WebView_CoreWebView2InitializationCompleted(object? sender, CoreWebView2InitializationCompletedEventArgs e)
{
if (e.IsSuccess)
@@ -143,6 +174,15 @@ public class WebpagePanel : UserControl
}));
}
public void NavigateToMd(string md, string backgroundColor = "#2b2b2b")
{
md = WebUtility.HtmlEncode(md);
string md2Html = ResourceHelper.GetString($"pack://application:,,,/Assets/Strings/md2html.html",
Encoding.UTF8);
var html = md2Html.Replace("{{content}}", md).Replace("#202020", backgroundColor);
NavigateToHtml(html);
}
private void NavigationStarting_CancelNavigation(object? sender, CoreWebView2NavigationStartingEventArgs e)
{
if (e.Uri.StartsWith("data:")) // when using NavigateToString

View File

@@ -43,9 +43,15 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:NavigationView x:Name="RootNavigation"
Grid.Column="0"
Grid.Row="1"
Margin="0,0,0,5"
IsBackButtonVisible="Collapsed"
IsPaneToggleVisible="True"
OpenPaneLength="160">

View File

@@ -7,6 +7,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:drawer="clr-namespace:BetterGenshinImpact.View.Controls.Drawer"
d:DataContext="{d:DesignInstance Type=pages:JsListViewModel}"
d:DesignHeight="600"
d:DesignWidth="800"
@@ -22,82 +23,93 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="42,16,42,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TextBlock Grid.Row="0"
Margin="0,0,0,8"
FontTypography="BodyStrong"
Text="自定义 Javascript 脚本(实验功能)" />
<ui:TextBlock Grid.Row="1"
Margin="0,0,0,8"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap">
可以通过 Javascript 调用 BetterGI 在原神中的各项能力。请在调度器中使用!<Hyperlink Command="{Binding GoToJsScriptUrlCommand}" Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
点击查看 Javascript 脚本使用与编写教程
</Hyperlink>
</ui:TextBlock>
<Grid>
<Grid Margin="42,16,42,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<ui:Button Command="{Binding OpenScriptsFolderCommand}"
Content="打开脚本目录"
Icon="{ui:SymbolIcon FolderOpen24}" />
<Separator Width="10" Opacity="0" />
<ui:Button Command="{Binding OpenLocalScriptRepoCommand}" Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"
Style="{DynamicResource DotInfoBadgeStyle}"
Visibility="{Binding Config.ScriptConfig.ScriptRepoHintDotVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</ui:Button>
</StackPanel>
<ui:TextBlock Grid.Row="0"
Margin="0,0,0,8"
FontTypography="BodyStrong"
Text="自定义 Javascript 脚本(实验功能)" />
<ui:TextBlock Grid.Row="1"
Margin="0,0,0,8"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap">
可以通过 Javascript 调用 BetterGI 在原神中的各项能力。请在调度器中使用!
<Hyperlink Command="{Binding GoToJsScriptUrlCommand}"
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
点击查看 Javascript 脚本使用与编写教程
</Hyperlink>
</ui:TextBlock>
<Separator Grid.Row="3"
Height="10"
Opacity="0" />
<StackPanel Grid.Row="2" Orientation="Horizontal">
<ui:Button Command="{Binding OpenScriptsFolderCommand}"
Content="打开脚本目录"
Icon="{ui:SymbolIcon FolderOpen24}" />
<Separator Width="10" Opacity="0" />
<ui:Button Command="{Binding OpenLocalScriptRepoCommand}" Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"
Style="{DynamicResource DotInfoBadgeStyle}"
Visibility="{Binding Config.ScriptConfig.ScriptRepoHintDotVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</ui:Button>
</StackPanel>
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
<!-- <ColumnDefinition Width="120" /> -->
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Grid x:Name="Col1" Grid.Column="0" />
<Grid x:Name="Col2" Grid.Column="1" />
<Grid x:Name="Col3" Grid.Column="2" />
<Grid x:Name="Col4" Grid.Column="3" />
<!-- <Grid x:Name="Col5" Grid.Column="4" /> -->
</Grid>
<ui:ListView Grid.Row="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding ScriptItems}"
SelectionMode="Single">
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource GridViewColumnHeaderDarkStyle}">
<GridViewColumn Width="{Binding ElementName=Col1, Path=ActualWidth}"
DisplayMemberBinding="{Binding FolderName}"
Header="目录" />
<GridViewColumn Width="{Binding ElementName=Col2, Path=ActualWidth}"
DisplayMemberBinding="{Binding Manifest.Name}"
Header="名称" />
<GridViewColumn Width="{Binding ElementName=Col3, Path=ActualWidth}"
DisplayMemberBinding="{Binding Manifest.Version}"
Header="版本" />
<!--<GridViewColumn Width="{Binding ElementName=Col4, Path=ActualWidth}" Header="操作">
<Separator Grid.Row="3"
Height="10"
Opacity="0" />
<Grid Grid.Row="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
<!-- <ColumnDefinition Width="120" /> -->
<ColumnDefinition Width="20" />
</Grid.ColumnDefinitions>
<Grid x:Name="Col1" Grid.Column="0" />
<Grid x:Name="Col2" Grid.Column="1" />
<Grid x:Name="Col3" Grid.Column="2" />
<Grid x:Name="Col4" Grid.Column="3" />
<!-- <Grid x:Name="Col5" Grid.Column="4" /> -->
</Grid>
<ui:ListView Grid.Row="5"
x:Name="ScriptsListView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding ScriptItems}"
SelectionMode="Single">
<b:Interaction.Triggers>
<b:EventTrigger EventName="SelectionChanged">
<b:InvokeCommandAction Command="{Binding OpenScriptDetailDrawerCommand}"
CommandParameter="{Binding ElementName=ScriptsListView, Path=SelectedItem}" />
</b:EventTrigger>
</b:Interaction.Triggers>
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource GridViewColumnHeaderDarkStyle}">
<GridViewColumn Width="{Binding ElementName=Col1, Path=ActualWidth}"
DisplayMemberBinding="{Binding FolderName}"
Header="目录" />
<GridViewColumn Width="{Binding ElementName=Col2, Path=ActualWidth}"
DisplayMemberBinding="{Binding Manifest.Name}"
Header="名称" />
<GridViewColumn Width="{Binding ElementName=Col3, Path=ActualWidth}"
DisplayMemberBinding="{Binding Manifest.Version}"
Header="版本" />
<!--<GridViewColumn Width="{Binding ElementName=Col4, Path=ActualWidth}" Header="操作">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
@@ -110,34 +122,55 @@
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>-->
</GridView>
</ListView.View>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding OpenScriptProjectFolderCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="打开目录" />
<MenuItem Command="{Binding StartRunCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="执行脚本" />
<MenuItem Command="{Binding RefreshCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="刷新" />
</ContextMenu>
</ListBox.ContextMenu>
<ListView.Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
</ListView.Style>
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource ListViewItemStyle}" TargetType="ui:ListViewItem">
<Setter Property="ToolTip" Value="{Binding Manifest.Description}" />
<Setter Property="ToolTipService.InitialShowDelay" Value="0" />
<Setter Property="ToolTipService.ShowDuration" Value="60000" />
</Style>
</ListView.ItemContainerStyle>
</ui:ListView>
</GridView>
</ListView.View>
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding OpenScriptProjectFolderCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="打开目录" />
<MenuItem Command="{Binding StartRunCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="执行脚本" />
<MenuItem Command="{Binding RefreshCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}, Path=PlacementTarget.SelectedItem}"
Header="刷新" />
</ContextMenu>
</ListBox.ContextMenu>
<ListView.Style>
<Style TargetType="{x:Type ListView}">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="Transparent" />
</Style>
</ListView.Style>
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource ListViewItemStyle}" TargetType="ui:ListViewItem">
<Setter Property="ToolTip" Value="{Binding Manifest.Description}" />
<Setter Property="ToolTipService.InitialShowDelay" Value="0" />
<Setter Property="ToolTipService.ShowDuration" Value="60000" />
</Style>
</ListView.ItemContainerStyle>
</ui:ListView>
</Grid>
<!-- 抽屉控件 -->
<drawer:CustomDrawer
IsOpen="{Binding DrawerVm.IsDrawerOpen, Mode=TwoWay}"
DrawerPosition="{Binding DrawerVm.DrawerPosition}"
OpenWidth="{Binding DrawerVm.DrawerWidth}"
OpenHeight="{Binding DrawerVm.DrawerHeight}"
DrawerBackground="{DynamicResource ControlFillColorDefaultBrush}"
Content="{Binding DrawerVm.DrawerContent}"
Panel.ZIndex="100">
<b:Interaction.Triggers>
<b:EventTrigger EventName="Opened">
<b:InvokeCommandAction Command="{Binding DrawerVm.OnDrawerOpenedCommand}"
PassEventArgsToCommand="True"/>
</b:EventTrigger>
<b:EventTrigger EventName="Closing">
<b:InvokeCommandAction Command="{Binding DrawerVm.OnDrawerClosingCommand}"
PassEventArgsToCommand="True"/>
</b:EventTrigger>
</b:Interaction.Triggers>
</drawer:CustomDrawer>
</Grid>
</UserControl>

View File

@@ -10,6 +10,7 @@
xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:vio="http://schemas.lepo.co/wpfui/2022/xaml/violeta"
xmlns:drawer="clr-namespace:BetterGenshinImpact.View.Controls.Drawer"
d:DataContext="{d:DesignInstance Type=pages:MapPathingViewModel}"
d:DesignHeight="600"
d:DesignWidth="800"
@@ -25,102 +26,135 @@
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="42,16,42,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid>
<Grid Margin="42,16,42,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TextBlock Grid.Row="0"
Margin="0,0,0,8"
FontTypography="BodyStrong"
Text="地图追踪(实验功能)" />
<ui:TextBlock Grid.Row="1"
Margin="0,0,0,8"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap">
可以实现自动采集、自动挖矿、自动锄地等功能。请在调度器中使用!<Hyperlink Command="{Binding GoToPathingUrlCommand}" Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
点击查看地图追踪与录制使用教程
</Hyperlink>
</ui:TextBlock>
<ui:TextBlock Grid.Row="0"
Margin="0,0,0,8"
FontTypography="BodyStrong"
Text="地图追踪(实验功能)" />
<ui:TextBlock Grid.Row="1"
Margin="0,0,0,8"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
TextWrapping="Wrap">
可以实现自动采集、自动挖矿、自动锄地等功能。请在调度器中使用!
<Hyperlink Command="{Binding GoToPathingUrlCommand}"
Foreground="{ui:ThemeResource TextFillColorSecondaryBrush}">
点击查看地图追踪与录制使用教程
</Hyperlink>
</ui:TextBlock>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<ui:Button Command="{Binding OpenScriptsFolderCommand}"
Content="打开任务目录"
Icon="{ui:SymbolIcon FolderOpen24}" />
<ui:Button Margin="10,0,0,0"
Command="{Binding OpenLocalScriptRepoCommand}"
Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"
Style="{DynamicResource DotInfoBadgeStyle}"
Visibility="{Binding Config.ScriptConfig.ScriptRepoHintDotVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
<StackPanel Grid.Row="2" Orientation="Horizontal">
<ui:Button Command="{Binding OpenScriptsFolderCommand}"
Content="打开任务目录"
Icon="{ui:SymbolIcon FolderOpen24}" />
<ui:Button Margin="10,0,0,0"
Command="{Binding OpenLocalScriptRepoCommand}"
Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"
Style="{DynamicResource DotInfoBadgeStyle}"
Visibility="{Binding Config.ScriptConfig.ScriptRepoHintDotVisible, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
</ui:Button>
<ui:Button Margin="10,0,0,0"
Command="{Binding OpenSettingsCommand}"
Content="设置"
Icon="{ui:SymbolIcon Settings24}" />
<ui:Button Margin="10,0,0,0"
Command="{Binding OpenDevToolsCommand}"
Content="开发者工具" />
</StackPanel>
<Separator Grid.Row="3"
Height="10"
Opacity="0" />
<ui:Border Grid.Row="5"
Background="{DynamicResource CardBackground}"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource ControlCornerRadius}">
<Grid Margin="4">
<ui:Grid ColumnDefinitions="*,10" Visibility="Hidden">
<Grid x:Name="TreeColumnStar" Grid.Column="0" />
</ui:Grid>
<ui:TreeListView BorderThickness="0"
ItemsSource="{Binding TreeList}"
SelectedItem="{Binding SelectNode, Mode=TwoWay}">
<b:Interaction.Triggers>
<b:EventTrigger EventName="SelectedItemChanged">
<b:InvokeCommandAction Command="{Binding OpenPathingDetailCommand}"
CommandParameter="{Binding SelectNode}" />
</b:EventTrigger>
</b:Interaction.Triggers>
<ui:TreeListView.Columns>
<GridViewColumnCollection>
<ui:GridViewColumn Width="{Binding ActualWidth, ElementName=TreeColumnStar}"
Header="名称">
<ui:GridViewColumn.CellTemplate>
<DataTemplate>
<ui:TreeRowExpander>
<ui:TreeRowExpander.Content>
<ui:StackPanel Margin="8,0,0,0"
Orientation="Horizontal"
Spacing="8">
<Image Height="16"
Source="{Binding IconFilePath, Converter={x:Static vio:PathToIconConverter.Instance}}" />
<TextBlock VerticalAlignment="Center" Text="{Binding FileName}" />
</ui:StackPanel>
</ui:TreeRowExpander.Content>
</ui:TreeRowExpander>
</DataTemplate>
</ui:GridViewColumn.CellTemplate>
</ui:GridViewColumn>
</GridViewColumnCollection>
</ui:TreeListView.Columns>
<ui:TreeListView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}" />
</ui:TreeListView.ItemTemplate>
<ui:TreeListView.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding StartCommand}" Header="执行任务" />
<MenuItem Command="{Binding RefreshCommand}" Header="刷新" />
</ContextMenu>
</ui:TreeListView.ContextMenu>
</ui:TreeListView>
</Grid>
</ui:Button>
<ui:Button Margin="10,0,0,0"
Command="{Binding OpenSettingsCommand}"
Content="设置"
Icon="{ui:SymbolIcon Settings24}" />
<ui:Button Margin="10,0,0,0"
Command="{Binding OpenDevToolsCommand}"
Content="开发者工具" />
</StackPanel>
</ui:Border>
</Grid>
<Separator Grid.Row="3"
Height="10"
Opacity="0" />
<ui:Border Grid.Row="5"
Background="{DynamicResource CardBackground}"
BorderBrush="{DynamicResource CardBorderBrush}"
BorderThickness="1"
CornerRadius="{DynamicResource ControlCornerRadius}">
<Grid Margin="4">
<ui:Grid ColumnDefinitions="*,10" Visibility="Hidden">
<Grid x:Name="TreeColumnStar" Grid.Column="0" />
</ui:Grid>
<ui:TreeListView BorderThickness="0"
ItemsSource="{Binding TreeList}"
SelectedItem="{Binding SelectNode, Mode=TwoWay}">
<ui:TreeListView.Columns>
<GridViewColumnCollection>
<ui:GridViewColumn Width="{Binding ActualWidth, ElementName=TreeColumnStar}" Header="名称">
<ui:GridViewColumn.CellTemplate>
<DataTemplate>
<ui:TreeRowExpander>
<ui:TreeRowExpander.Content>
<ui:StackPanel Margin="8,0,0,0"
Orientation="Horizontal"
Spacing="8">
<Image Height="16" Source="{Binding IconFilePath, Converter={x:Static vio:PathToIconConverter.Instance}}" />
<TextBlock VerticalAlignment="Center" Text="{Binding FileName}" />
</ui:StackPanel>
</ui:TreeRowExpander.Content>
</ui:TreeRowExpander>
</DataTemplate>
</ui:GridViewColumn.CellTemplate>
</ui:GridViewColumn>
</GridViewColumnCollection>
</ui:TreeListView.Columns>
<ui:TreeListView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}" />
</ui:TreeListView.ItemTemplate>
<ui:TreeListView.ContextMenu>
<ContextMenu>
<MenuItem Command="{Binding StartCommand}" Header="执行任务" />
<MenuItem Command="{Binding RefreshCommand}" Header="刷新" />
</ContextMenu>
</ui:TreeListView.ContextMenu>
</ui:TreeListView>
</Grid>
</ui:Border>
<!-- 添加抽屉控件 -->
<drawer:CustomDrawer
IsOpen="{Binding DrawerVm.IsDrawerOpen, Mode=TwoWay}"
DrawerPosition="{Binding DrawerVm.DrawerPosition}"
OpenWidth="{Binding DrawerVm.DrawerWidth}"
OpenHeight="{Binding DrawerVm.DrawerHeight}"
DrawerBackground="{DynamicResource ControlFillColorDefaultBrush}"
Content="{Binding DrawerVm.DrawerContent}"
Panel.ZIndex="100">
<b:Interaction.Triggers>
<b:EventTrigger EventName="Opened">
<b:InvokeCommandAction Command="{Binding DrawerVm.OnDrawerOpenedCommand}"
PassEventArgsToCommand="True" />
</b:EventTrigger>
<b:EventTrigger EventName="Closing">
<b:InvokeCommandAction Command="{Binding DrawerVm.OnDrawerClosingCommand}"
PassEventArgsToCommand="True" />
</b:EventTrigger>
</b:Interaction.Triggers>
</drawer:CustomDrawer>
</Grid>
</UserControl>

View File

@@ -335,13 +335,18 @@
<!-- 自动秘境 -->
<TextBlock Margin="4,0,0,4"
FontSize="14"
Foreground="{DynamicResource SystemAccentColorPrimaryBrush}"
Text="自动秘境" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" Margin="4,0,0,4">
<TextBlock FontSize="14"
Foreground="{DynamicResource SystemAccentColorPrimaryBrush}"
Text="自动秘境" />
<TextBlock Margin="4,0,0,0"
VerticalAlignment="Center"
FontSize="11"
Text="(此处未覆盖的配置可在 独立任务-自动秘境 中配置)" />
</StackPanel>
<ui:CardExpander Margin="0,0,0,12" ContentPadding="0" IsExpanded="{Binding SelectedConfig.WeeklyDomainEnabled, Converter={StaticResource InverseBooleanConverter}, Mode=TwoWay}">
<ui:CardExpander Margin="0,0,0,12" ContentPadding="0">
<ui:CardExpander.Icon>
<ui:FontIcon Glyph="&#xf073;" Style="{StaticResource FaFontIconStyle}" />
</ui:CardExpander.Icon>
@@ -470,7 +475,8 @@
</Grid>
</StackPanel>
</ui:CardExpander>
<ui:CardExpander Margin="0,0,0,12" ContentPadding="0" IsExpanded="{Binding SelectedConfig.WeeklyDomainEnabled, Mode=TwoWay}">
<ui:CardExpander Margin="0,0,0,12" ContentPadding="0">
<ui:CardExpander.Icon>
<ui:FontIcon Glyph="&#xf784;" Style="{StaticResource FaFontIconStyle}" />
</ui:CardExpander.Icon>
@@ -502,25 +508,20 @@
</Grid>
</ui:CardExpander.Header>
<StackPanel>
<Grid Margin="52,16,16,8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
Margin="0,20,0,0"
HorizontalAlignment="Center"
FontTypography="Body"
Text=""
TextWrapping="Wrap">
新的一天开始于 4:00
</ui:TextBlock>
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
FontSize="12"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="周期始于凌晨 4:00 ,如周一 4:00 至周二 3:59 刷取周一秘境"
TextWrapping="Wrap">
周一 4:00 至周二 3:59 执行周一配置,以此类推。
</ui:TextBlock>
</Grid>
<Grid Margin="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@@ -841,6 +842,9 @@
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
TextAlignment="Center"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="用于给指定队伍加好感度"
TextWrapping="Wrap" />
@@ -852,6 +856,9 @@
Margin="0,0,28,0"
Text="{Binding SelectedConfig.DailyRewardPartyName, Mode=TwoWay}"
PlaceholderText="填写好感队名称"
TextAlignment="Center"
VerticalAlignment="Center"
HorizontalAlignment="Center"
TextWrapping="Wrap" />
</ui:CardControl>

View File

@@ -9,6 +9,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="clr-namespace:BetterGenshinImpact.ViewModel.Pages"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
Title="TaskSettingsPage"
d:DataContext="{d:DesignInstance Type=pages:TaskSettingsPageViewModel}"
d:DesignHeight="1150"
@@ -954,20 +955,113 @@
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="循环次数"
Text="刷取至树脂耗尽"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="循环秘境多少次,输入 0 则为用光所有树脂为止,优先使用浓缩树脂"
Text="优先使用浓缩树脂,然后使用原粹树脂,其余树脂不使用"
TextWrapping="Wrap" />
<ui:TextBox Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
MinWidth="90"
Margin="0,0,36,0"
Text="{Binding AutoDomainRoundNum, Mode=TwoWay}" />
<ui:ToggleSwitch Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Margin="0,0,36,0"
IsChecked="{Binding Config.AutoDomainConfig.SpecifyResinUse,Converter={StaticResource InverseBooleanConverter}, Mode=TwoWay}" />
</Grid>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 指定树脂使用开关 -->
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="指定每种树脂刷取次数"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="开启后会根据配置的次数使用对应的树脂"
TextWrapping="Wrap" />
<ui:ToggleSwitch Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
Margin="0,0,36,0"
IsChecked="{Binding Config.AutoDomainConfig.SpecifyResinUse, Mode=TwoWay}" />
</Grid>
<!-- 树脂设置组 -->
<Border Margin="16,8,16,16"
BorderThickness="1"
BorderBrush="{ui:ThemeResource CardStrokeColorDefaultBrush}"
Background="{ui:ThemeResource ControlFillColorSecondaryBrush}"
CornerRadius="8"
IsEnabled="{Binding Config.AutoDomainConfig.SpecifyResinUse, Mode=OneWay}">
<StackPanel Margin="12">
<!-- 原粹树脂设置 -->
<StackPanel Orientation="Horizontal"
Margin="0,8,0,12">
<ui:TextBlock Text="原粹树脂刷取次数:"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<ui:NumberBox Value="{Binding Config.AutoDomainConfig.OriginalResinUseCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Minimum="0"
SmallChange="1"
LargeChange="5"
SpinButtonPlacementMode="Inline"
Width="120"/>
</StackPanel>
<!-- 浓缩树脂设置 -->
<StackPanel Orientation="Horizontal"
Margin="0,0,0,12">
<ui:TextBlock Text="浓缩树脂刷取次数:"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<ui:NumberBox Value="{Binding Config.AutoDomainConfig.CondensedResinUseCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Minimum="0"
SmallChange="1"
LargeChange="5"
SpinButtonPlacementMode="Inline"
Width="120" />
</StackPanel>
<!-- 须臾树脂设置 -->
<StackPanel Orientation="Horizontal"
Margin="0,0,0,12">
<ui:TextBlock Text="须臾树脂刷取次数:"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<ui:NumberBox Value="{Binding Config.AutoDomainConfig.TransientResinUseCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Minimum="0"
SmallChange="1"
LargeChange="5"
SpinButtonPlacementMode="Inline"
Width="120" />
</StackPanel>
<!-- 脆弱树脂设置 -->
<StackPanel Orientation="Horizontal"
Margin="0,0,0,0">
<ui:TextBlock Text="脆弱树脂刷取次数:"
VerticalAlignment="Center"
Margin="0,0,8,0" />
<ui:NumberBox Value="{Binding Config.AutoDomainConfig.FragileResinUseCount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Minimum="0"
SmallChange="1"
LargeChange="5"
SpinButtonPlacementMode="Inline"
Width="120" />
</StackPanel>
</StackPanel>
</Border>
<Grid Margin="16">
<Grid.RowDefinitions>

View File

@@ -878,6 +878,32 @@
Margin="0,0,36,0"
IsChecked="{Binding PathingConfig.AutoFightConfig.KazuhaPickupEnabled, Mode=TwoWay}" />
</Grid>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0"
Grid.Column="0"
FontTypography="Body"
Text="拾取战斗人次阈值"
TextWrapping="Wrap" />
<ui:TextBlock Grid.Row="1"
Grid.Column="0"
Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}"
Text="拾取战斗人次阈值,当战斗人次小于一定次数就结束战斗情况下不触发拾取掉落物和万叶拾取配置只有不小于2时才生效。"
TextWrapping="Wrap" />
<ui:TextBox Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
MinWidth="120"
Margin="0,0,36,0"
Text="{Binding PathingConfig.AutoFightConfig.BattleThresholdForLoot}" />
</Grid>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />

View File

@@ -135,11 +135,11 @@ public partial class CheckUpdateWindow : FluentWindow
if (_option.Channel == UpdateChannel.Stable)
{
await RunUpdaterAsync("--source mirrorc");
await RunUpdaterAsync("-I --source mirrorc");
}
else
{
await RunUpdaterAsync("--source mirrorc-alpha");
await RunUpdaterAsync("-I --source mirrorc-alpha");
}
}

View File

@@ -0,0 +1,86 @@
<ui:FluentWindow x:Class="BetterGenshinImpact.View.Windows.ScriptRepoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:BetterGenshinImpact.View.Windows"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:vio="http://schemas.lepo.co/wpfui/2022/xaml/violeta"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
Title="脚本仓库"
Width = "350"
MinWidth="350"
MinHeight="50"
SizeToContent="Height"
Background="#202020"
ExtendsContentIntoTitleBar="True"
FontFamily="{DynamicResource TextThemeFontFamily}"
WindowBackdropType="None"
WindowStartupLocation="CenterOwner"
mc:Ignorable="d">
<Grid>
<ui:Grid Margin="0,48,0,0">
<Border Grid.Column="2"
Margin="6"
Background="{ui:ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ui:ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1,1,1,1"
CornerRadius="8">
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 第一行:更新渠道 -->
<StackPanel Grid.Row="0"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="0,0,0,12">
<ui:TextBlock
Foreground="{ui:ThemeResource TextFillColorPrimaryBrush}"
VerticalAlignment="Center"
Margin="0,0,8,0"
Text="更新渠道:" />
<ComboBox
Width="160"
ItemsSource="{Binding RepoChannels}"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedRepoChannel}"
VerticalAlignment="Center"/>
</StackPanel>
<!-- 第二行:脚本仓库 -->
<StackPanel Grid.Row="1"
Orientation="Horizontal" >
<ui:Button
Icon="{ui:SymbolIcon CloudSync24}"
Content="更新仓库"
Margin="0,0,8,0"
Command="{Binding UpdateRepoCommand}" />
<ui:Button
Icon="{ui:SymbolIcon ArrowReset24}"
Content="重置仓库"
Margin="0,0,8,0"
Command="{Binding ResetRepoCommand}" />
<ui:Button
Icon="{ui:SymbolIcon BookStar24}"
Appearance="Primary"
Content="打开仓库"
Command="{Binding OpenLocalScriptRepoCommand}" />
</StackPanel>
</Grid>
</Border>
</ui:Grid>
<ui:TitleBar Title="{Binding Title}"
ShowMaximize="False"
ShowMinimize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="pack://application:,,,/Assets/Images/logo.png" />
</ui:TitleBar.Icon>
</ui:TitleBar>
</Grid>
</ui:FluentWindow>

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.Helpers;
using Wpf.Ui.Violeta.Controls;
namespace BetterGenshinImpact.View.Windows;
[ObservableObject]
public partial class ScriptRepoWindow
{
// 更新渠道类
public class RepoChannel
{
public string Name { get; set; }
public string Url { get; set; }
public RepoChannel(string name, string url)
{
Name = name;
Url = url;
}
}
// 渠道列表
private ObservableCollection<RepoChannel> _repoChannels;
public ObservableCollection<RepoChannel> RepoChannels => _repoChannels;
// 选中的渠道
[ObservableProperty] private RepoChannel _selectedRepoChannel;
public ScriptRepoWindow()
{
InitializeRepoChannels();
InitializeComponent();
DataContext = this;
}
private void InitializeRepoChannels()
{
_repoChannels = new ObservableCollection<RepoChannel>
{
new RepoChannel("CNB", "https://cnb.cool/bettergi/bettergi-scripts-list"),
new RepoChannel("GitCode", "https://gitcode.com/huiyadanli/bettergi-scripts-list"),
new RepoChannel("Gitee", "https://gitee.com/babalae/bettergi-scripts-list"),
new RepoChannel("GitHub", "https://github.com/babalae/bettergi-scripts-list"),
};
SelectedRepoChannel = _repoChannels[0];
}
[RelayCommand]
private async Task UpdateRepo()
{
try
{
// 使用选定渠道的URL进行更新
string repoUrl = SelectedRepoChannel.Url;
// 显示更新中提示
Toast.Information("正在更新脚本仓库...");
// 执行更新
var (repoPath, updated) = await ScriptRepoUpdater.Instance.UpdateCenterRepoByGit(repoUrl);
// 更新结果提示
if (updated)
{
Toast.Success("脚本仓库更新成功,有新内容");
}
else
{
Toast.Success("脚本仓库已是最新");
}
}
catch (Exception ex)
{
await MessageBox.ErrorAsync($"更新失败,可尝试重置仓库后重新更新。失败原因:: {ex.Message}");
}
}
[RelayCommand]
private void OpenLocalScriptRepo()
{
TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
Close();
}
[RelayCommand]
private async Task ResetRepo()
{
// 添加确认对话框
var result = await MessageBox.ShowAsync(
"确定要重置脚本仓库吗?无法正常更新时候可以使用本功能,重置后请重新更新脚本仓库。",
"确认重置",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
try
{
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath))
{
DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPath);
Toast.Success("脚本仓库已重置,请重新更新脚本仓库。");
}
else
{
Toast.Information("脚本仓库不存在,无需重置");
}
}
catch (Exception ex)
{
Toast.Error($"重置失败: {ex.Message}");
}
}
}
}

View File

@@ -149,7 +149,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel
}
// 更新仓库
ScriptRepoUpdater.Instance.AutoUpdate();
// ScriptRepoUpdater.Instance.AutoUpdate();
// 清理临时目录
TempManager.CleanUp();

View File

@@ -226,15 +226,16 @@ public partial class CommonSettingsPageViewModel : ViewModel
{
var zipPath = dialog.FileName;
// 删除旧文件夹
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath))
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld))
{
DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPath);
DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPathOld);
}
ZipFile.ExtractToDirectory(zipPath, ScriptRepoUpdater.ReposPath, true);
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPath))
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld))
{
DirectoryHelper.CopyDirectory(ScriptRepoUpdater.CenterRepoPathOld, ScriptRepoUpdater.CenterRepoPath);
MessageBox.Information("脚本仓库离线包导入成功!");
}
else

View File

@@ -9,15 +9,25 @@ using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using BetterGenshinImpact.Helpers;
using BetterGenshinImpact.View.Controls.Drawer;
using BetterGenshinImpact.View.Controls.Webview;
using BetterGenshinImpact.ViewModel.Message;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Web.WebView2.Wpf;
using Wpf.Ui;
using Wpf.Ui.Controls;
using Wpf.Ui.Violeta.Controls;
using Button = Wpf.Ui.Controls.Button;
using StackPanel = Wpf.Ui.Controls.StackPanel;
using TextBlock = Wpf.Ui.Controls.TextBlock;
using TextBox = Wpf.Ui.Controls.TextBox;
namespace BetterGenshinImpact.ViewModel.Pages;
@@ -26,13 +36,21 @@ public partial class JsListViewModel : ViewModel
private readonly ILogger<JsListViewModel> _logger = App.GetLogger<JsListViewModel>();
private readonly string scriptPath = Global.ScriptPath();
[ObservableProperty]
private ObservableCollection<ScriptProject> _scriptItems = [];
[ObservableProperty] private ObservableCollection<ScriptProject> _scriptItems = [];
private readonly IScriptService _scriptService;
public AllConfig Config { get; set; }
public DrawerViewModel DrawerVm { get; } = new DrawerViewModel();
private WebView2? _webView2;
private WebpagePanel? _mdWebpagePanel;
private TaskCompletionSource<bool>? _navigationCompletionSource;
private const int NavigationTimeoutMs = 10000; // 10秒超时
public JsListViewModel(IScriptService scriptService, IConfigService configService)
{
_scriptService = scriptService;
@@ -114,13 +132,214 @@ public partial class JsListViewModel : ViewModel
[RelayCommand]
public void OnGoToJsScriptUrl()
{
Process.Start(new ProcessStartInfo("https://bettergi.com/feats/autos/jsscript.html") { UseShellExecute = true });
Process.Start(new ProcessStartInfo("https://bettergi.com/feats/autos/jsscript.html")
{ UseShellExecute = true });
}
[RelayCommand]
public void OnOpenLocalScriptRepo()
{
Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
}
[RelayCommand]
private void OpenScriptDetailDrawer(object? scriptItem)
{
if (scriptItem == null)
{
return;
}
if (scriptItem is ScriptProject scriptProject)
{
// 检查是否存在README.md或其他md文件
var mdFilePath = FindMdFilePath(scriptProject);
// 设置抽屉位置和大小
DrawerVm.DrawerPosition = DrawerPosition.Right;
if (!string.IsNullOrEmpty(mdFilePath))
{
DrawerVm.DrawerWidth = 450;
// 注册抽屉关闭前事件
DrawerVm.SetDrawerClosingAction(args =>
{
if (_mdWebpagePanel != null)
{
_mdWebpagePanel.Visibility = Visibility.Hidden;
}
});
DrawerVm.setDrawerOpenedAction(async () =>
{
if (_mdWebpagePanel != null)
{
// 等待导航完成或超时
try
{
await WaitForNavigationCompletedWithTimeout();
_mdWebpagePanel.Visibility = Visibility.Visible;
_mdWebpagePanel.WebView.Focus();
Debug.WriteLine("Navigation completed successfully");
// 导航成功完成后执行其他操作
}
catch (TimeoutException)
{
Toast.Error("Markdown内容加载超时");
}
}
});
}
else
{
DrawerVm.SetDrawerClosingAction(_ => { });
DrawerVm.setDrawerOpenedAction(() => { });
DrawerVm.DrawerWidth = 300;
}
// 创建要在抽屉中显示的内容
var content = CreateScriptDetailContent(scriptProject, mdFilePath);
// 打开抽屉
DrawerVm.OpenDrawer(content);
}
}
private async Task WaitForNavigationCompletedWithTimeout()
{
var completedTask = await Task.WhenAny(
_navigationCompletionSource!.Task,
Task.Delay(NavigationTimeoutMs)
);
if (completedTask != _navigationCompletionSource.Task)
{
throw new TimeoutException("Navigation did not complete within the timeout period");
}
}
private object CreateScriptDetailContent(ScriptProject scriptProject, string? mdFilePath)
{
// 创建显示脚本详情的控件
var border = new Border
{
Background = new SolidColorBrush(Color.FromRgb(0x2B, 0x2B, 0x2B)),
Padding = new Thickness(20)
};
var panel = new StackPanel();
border.Child = panel;
// 假设scriptItem是你的脚本对象根据实际类型进行调整
panel.Children.Add(new TextBlock
{
Text = scriptProject.Manifest.Name,
FontSize = 20,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 10)
});
// 如果找到md文件使用WebpagePanel显示
if (!string.IsNullOrEmpty(mdFilePath))
{
// 使用Grid作为容器来实现填充效果
var grid = new Grid
{
Margin = new Thickness(0, 0, 0, 15)
};
_mdWebpagePanel = new WebpagePanel
{
Margin = new Thickness(0),
Visibility = Visibility.Hidden,
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
_navigationCompletionSource = new TaskCompletionSource<bool>();
_mdWebpagePanel.OnNavigationCompletedAction = (_) =>
{
// 导航完成时设置任务结果
_navigationCompletionSource.TrySetResult(true);
};
_mdWebpagePanel.NavigateToMd(File.ReadAllText(mdFilePath));
grid.Children.Add(_mdWebpagePanel);
panel.Children.Add(grid);
// 设置Grid高度以占满剩余空间
panel.SizeChanged += (sender, args) =>
{
// 计算其他元素使用的高度
double otherElementsHeight = 0;
foreach (var child in panel.Children)
{
if (child != grid)
{
var frameworkElement = child as FrameworkElement;
if (frameworkElement != null)
{
otherElementsHeight += frameworkElement.ActualHeight + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom;
}
}
}
// 设置Grid高度为剩余空间
grid.Height = Math.Max(400, panel.ActualHeight - otherElementsHeight - 15); // 设置最小高度为400
};
}
else
{
panel.Children.Add(new TextBlock
{
Text = $"版本: {scriptProject.Manifest.Version}",
Margin = new Thickness(0, 5, 0, 5)
});
panel.Children.Add(new TextBlock
{
Text = scriptProject.Manifest.Description,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 5, 0, 15)
});
}
// 添加操作按钮
// var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal };
//
// var runButton = new Button
// {
// Content = "执行脚本",
// Margin = new Thickness(0, 0, 10, 0)
// };
// runButton.Click += async (s, e) => await OnStartRun(script);
// buttonPanel.Children.Add(runButton);
//
// var openFolderButton = new Button { Content = "打开目录" };
// openFolderButton.Click += (s, e) => OnOpenScriptProjectFolder(script);
// buttonPanel.Children.Add(openFolderButton);
// panel.Children.Add(buttonPanel);
return border;
}
private static string? FindMdFilePath(ScriptProject script)
{
string[] possibleMdFiles = { "README.md", "readme.md" };
string mdFilePath = null;
foreach (var mdFile in possibleMdFiles)
{
string fullPath = Path.Combine(script.ProjectPath, mdFile);
if (File.Exists(fullPath))
{
mdFilePath = fullPath;
break;
}
}
return mdFilePath;
}
}

View File

@@ -231,6 +231,6 @@ public partial class KeyMouseRecordPageViewModel : ViewModel
public void OnOpenLocalScriptRepo()
{
Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
}

View File

@@ -1,4 +1,5 @@
using BetterGenshinImpact.Core.Config;
using System;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script.Group;
using BetterGenshinImpact.Core.Script.Project;
using BetterGenshinImpact.GameTask.AutoPathing;
@@ -21,6 +22,14 @@ using Wpf.Ui.Violeta.Controls;
using BetterGenshinImpact.View.Pages.View;
using BetterGenshinImpact.ViewModel.Pages.View;
using Wpf.Ui.Violeta.Win32;
using BetterGenshinImpact.View.Controls.Drawer;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.ComponentModel;
using BetterGenshinImpact.View.Controls.Webview;
using Microsoft.Web.WebView2.Wpf;
using BetterGenshinImpact.Helpers;
namespace BetterGenshinImpact.ViewModel.Pages;
@@ -39,6 +48,15 @@ public partial class MapPathingViewModel : ViewModel
private readonly IScriptService _scriptService;
public AllConfig Config { get; set; }
// 添加抽屉ViewModel
public DrawerViewModel DrawerVm { get; } = new DrawerViewModel();
// 添加WebView2相关成员变量
private WebView2? _webView2;
private WebpagePanel? _mdWebpagePanel;
private TaskCompletionSource<bool>? _navigationCompletionSource;
private const int NavigationTimeoutMs = 10000; // 10秒超时
/// <inheritdoc/>
public MapPathingViewModel(IScriptService scriptService, IConfigService configService)
@@ -179,6 +197,222 @@ public partial class MapPathingViewModel : ViewModel
public void OnOpenLocalScriptRepo()
{
Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
}
[RelayCommand]
public void OnOpenPathingDetail()
{
var item = SelectNode;
if (item == null)
{
return;
}
// 如果是目录检查是否存在README.md
string? mdFilePath = null;
if (item.IsDirectory && !string.IsNullOrEmpty(item.FilePath))
{
mdFilePath = FindMdFilePath(item.FilePath);
}
// 设置抽屉位置和大小
DrawerVm.DrawerPosition = DrawerPosition.Right;
if (!string.IsNullOrEmpty(mdFilePath))
{
DrawerVm.DrawerWidth = 450;
// 注册抽屉关闭前事件
DrawerVm.SetDrawerClosingAction(args =>
{
if (_mdWebpagePanel != null)
{
_mdWebpagePanel.Visibility = Visibility.Hidden;
}
});
DrawerVm.setDrawerOpenedAction(async () =>
{
if (_mdWebpagePanel != null)
{
// 等待导航完成或超时
try
{
await WaitForNavigationCompletedWithTimeout();
_mdWebpagePanel.Visibility = Visibility.Visible;
_mdWebpagePanel.WebView.Focus();
Debug.WriteLine("Navigation completed successfully");
}
catch (TimeoutException)
{
Toast.Error("Markdown内容加载超时");
}
}
});
}
else
{
DrawerVm.DrawerWidth = 350;
DrawerVm.SetDrawerClosingAction(_ => { });
DrawerVm.setDrawerOpenedAction(() => { });
}
// 创建要在抽屉中显示的内容
var content = CreatePathingDetailContent(item, mdFilePath);
// 打开抽屉
if (content != null)
{
DrawerVm.OpenDrawer(content);
}
}
private async Task WaitForNavigationCompletedWithTimeout()
{
var completedTask = await Task.WhenAny(
_navigationCompletionSource!.Task,
Task.Delay(NavigationTimeoutMs)
);
if (completedTask != _navigationCompletionSource.Task)
{
throw new TimeoutException("Navigation did not complete within the timeout period");
}
}
private string? FindMdFilePath(string dirPath)
{
string[] possibleMdFiles = { "README.md", "readme.md" };
foreach (var mdFile in possibleMdFiles)
{
string fullPath = Path.Combine(dirPath, mdFile);
if (File.Exists(fullPath))
{
return fullPath;
}
}
return null;
}
private object? CreatePathingDetailContent(FileTreeNode<PathingTask> node, string? mdFilePath = null)
{
// 创建显示路径任务详情的控件
var border = new Border
{
Background = new SolidColorBrush(Color.FromRgb(0x2B, 0x2B, 0x2B)),
Padding = new Thickness(20)
};
var panel = new StackPanel();
border.Child = panel;
// 添加标题
panel.Children.Add(new TextBlock
{
Text = node.FileName,
FontSize = 20,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 0, 0, 10)
});
// 如果找到md文件使用WebpagePanel显示
if (!string.IsNullOrEmpty(mdFilePath))
{
// 使用Grid作为容器来实现填充效果
var grid = new Grid
{
Margin = new Thickness(0, 0, 0, 15)
};
_mdWebpagePanel = new WebpagePanel
{
Margin = new Thickness(0),
Visibility = Visibility.Hidden,
VerticalAlignment = VerticalAlignment.Stretch,
HorizontalAlignment = HorizontalAlignment.Stretch
};
_navigationCompletionSource = new TaskCompletionSource<bool>();
_mdWebpagePanel.OnNavigationCompletedAction = (_) =>
{
// 导航完成时设置任务结果
_navigationCompletionSource.TrySetResult(true);
};
_mdWebpagePanel.NavigateToMd(File.ReadAllText(mdFilePath));
grid.Children.Add(_mdWebpagePanel);
panel.Children.Add(grid);
// 设置Grid高度以占满剩余空间
panel.SizeChanged += (sender, args) =>
{
// 计算其他元素使用的高度
double otherElementsHeight = 0;
foreach (var child in panel.Children)
{
if (child != grid)
{
var frameworkElement = child as FrameworkElement;
if (frameworkElement != null)
{
otherElementsHeight += frameworkElement.ActualHeight + frameworkElement.Margin.Top + frameworkElement.Margin.Bottom;
}
}
}
// 设置Grid高度为剩余空间
grid.Height = Math.Max(400, panel.ActualHeight - otherElementsHeight - 15); // 设置最小高度为400
};
}
else if (!node.IsDirectory && !string.IsNullOrEmpty(node.FilePath))
{
// 如果是文件而不是目录,显示更多详情
try
{
if (string.IsNullOrEmpty(node.Value?.Info.Description))
{
return null;
}
panel.Children.Add(new TextBlock
{
Text = $"{node.Value?.Info.Description}",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 5, 0, 5)
});
}
catch (Exception ex)
{
panel.Children.Add(new TextBlock
{
Text = $"读取文件信息时出错: {ex.Message}",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 5, 0, 5)
});
}
}
else
{
// 显示目录信息
panel.Children.Add(new TextBlock
{
Text = "这是一个目录,包含多个地图追踪任务。",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 5, 0, 15)
});
// 添加子项信息
if (node.Children.Count > 0)
{
panel.Children.Add(new TextBlock
{
Text = $"包含 {node.Children.Count} 个子项",
Margin = new Thickness(0, 5, 0, 5)
});
}
}
return border;
}
}

View File

@@ -451,7 +451,7 @@ public partial class ScriptControlViewModel : ViewModel
public void OnOpenLocalScriptRepo()
{
TaskContext.Instance().Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
[RelayCommand]
@@ -1016,20 +1016,31 @@ public partial class ScriptControlViewModel : ViewModel
}
[RelayCommand]
public void OnDeleteScriptByFolder(ScriptGroupProject? item)
public async void OnDeleteScriptByFolder(ScriptGroupProject? item)
{
if (item == null)
{
return;
}
var toBeDeletedProjects = SelectedScriptGroup?.Projects.ToList().Where(item2 => item2.FolderName == item.FolderName);
if (toBeDeletedProjects != null)
}
if (SelectedScriptGroup != null)
{
var toBeDeletedProjects = SelectedScriptGroup.Projects
.Where(item2 => item2.FolderName == item.FolderName)
.ToList();
foreach (var project in toBeDeletedProjects)
{
OnDeleteScript(project);
}
SelectedScriptGroup.Projects.Remove(project);
}
_snackbarService.Show(
"脚本配置移除成功",
$"已移除 {item.FolderName} 下的所有关联配置",
ControlAppearance.Success,
null,
TimeSpan.FromSeconds(2)
);
}
}

View File

@@ -31,6 +31,7 @@ using BetterGenshinImpact.ViewModel.Pages.View;
using System.Linq;
using System.Reflection;
using System.Collections.Frozen;
using System.Diagnostics;
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
using BetterGenshinImpact.View.Windows;
@@ -162,13 +163,16 @@ public partial class TaskSettingsPageViewModel : ViewModel
[RelayCommand]
private async Task OnSOneDragonFlow()
{
if (OneDragonFlowViewModel == null || OneDragonFlowViewModel.SelectedConfig == null)
{
Toast.Warning("未设置任务!");
return;
}
OneDragonFlowViewModel.OnNavigatedTo();
await OneDragonFlowViewModel.OnOneKeyExecute();
if (OneDragonFlowViewModel == null || OneDragonFlowViewModel.SelectedConfig == null)
{
OneDragonFlowViewModel.OnNavigatedTo();
if (OneDragonFlowViewModel == null || OneDragonFlowViewModel.SelectedConfig == null)
{
Toast.Warning("未设置任务!");
return;
}
}
await OneDragonFlowViewModel.OnOneKeyExecute();
}
[RelayCommand]

View File

@@ -79,7 +79,7 @@ public partial class AutoFightViewModel : ObservableObject, IViewModel
public void OnOpenLocalScriptRepo()
{
Config.ScriptConfig.ScriptRepoHintDotVisible = false;
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
}
[RelayCommand]

View File

@@ -8,12 +8,12 @@
{
"id": "mirrorc",
"name": "Mirror酱",
"uri": "mirrorc://BGI"
"uri": "mirrorc://BGI?os=win&arch=x64"
},
{
"id": "mirrorc-alpha",
"name": "Mirror酱 Alpha",
"uri": "mirrorc://BGI?channel=alpha",
"uri": "mirrorc://BGI?channel=alpha&os=win&arch=x64",
"hidden": true
}
],

View File

@@ -130,4 +130,4 @@ BetterGI · 更好的原神, 一个基于计算机视觉技术,意图让原
## 问题反馈
提 [Issue](https://github.com/babalae/better-genshin-impact/issues) 或 QQ群[1036100501](https://qm.qq.com/q/fvRNqEbFyo)
提 [Issue](https://github.com/babalae/better-genshin-impact/issues) 或 QQ群[1053273766](https://qm.qq.com/q/qtocsOXnIQ)

View File

@@ -16,6 +16,8 @@ public class AvatarClassifyGen
private static readonly string BackgroundDir = @"E:\HuiTask\更好的原神\数据源\background";
private static readonly Random Rd = new Random();
public static readonly List<string> ImgNames = ["UI_AvatarIcon_Side_Ambor.png","UI_AvatarIcon_Side_AmborCostumeWic.png"];
public static void GenAll()
{
@@ -23,9 +25,8 @@ public class AvatarClassifyGen
// List<string> sideImageFiles = Directory.GetFiles(Path.Combine(BaseDir, "side_src"), "*.png", SearchOption.TopDirectoryOnly).ToList();
// 只用一个图像
List<string> sideImageFiles = [];
List<string> imgNames = ["UI_AvatarIcon_Side_Escoffier.png","UI_AvatarIcon_Side_Ifa.png",
"UI_AvatarIcon_Side_Flamingo.png","UI_AvatarIcon_Side_Hookwalker.png","UI_AvatarIcon_Side_Mosasaurus.png","UI_AvatarIcon_Side_Shamansaurus.png","UI_AvatarIcon_Side_Drillhead.png"];
foreach (string imgName in imgNames)
foreach (string imgName in ImgNames)
{
sideImageFiles.Add(Path.Combine(BaseDir, imgName));
}

View File

@@ -23,12 +23,7 @@ public class AvatarClassifyTransparentGen
public static void GenAll()
{
List<string> sideImageFiles = [];
List<string> imgNames =
[
"UI_AvatarIcon_Side_Escoffier.png", "UI_AvatarIcon_Side_Ifa.png",
"UI_AvatarIcon_Side_Flamingo.png", "UI_AvatarIcon_Side_Hookwalker.png", "UI_AvatarIcon_Side_Mosasaurus.png",
"UI_AvatarIcon_Side_Shamansaurus.png", "UI_AvatarIcon_Side_Drillhead.png"
];
List<string> imgNames = AvatarClassifyGen.ImgNames;
foreach (string imgName in imgNames)
{
sideImageFiles.Add(Path.Combine(BaseDir, imgName));

View File

@@ -10,13 +10,13 @@ public class OcrTest
{
public static void TestYap()
{
Mat mat = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png");
var text = TextInferenceFactory.Pick.Inference(TextInferenceFactory.PreProcessForInference(mat));
Debug.WriteLine(text);
Mat mat2 = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png");
var text2 = OcrFactory.Paddle.Ocr(mat2);
Debug.WriteLine(text2);
// Mat mat = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png");
// var text = TextInferenceFactory.Pick.Inference(TextInferenceFactory.PreProcessForInference(mat));
// Debug.WriteLine(text);
//
// Mat mat2 = Cv2.ImRead(@"E:\HuiTask\更好的原神\临时文件\fuben_jueyuan.png");
// var text2 = OcrFactory.Paddle.Ocr(mat2);
// Debug.WriteLine(text2);
}

View File

@@ -1,5 +1,6 @@
using BetterGenshinImpact.GameTask.AutoArtifactSalvage;
using BetterGenshinImpact.UnitTest.CoreTests.RecognitionTests.OCRTests;
using GameTask.Model.GameUI;
using OpenCvSharp;
using System;
using System.Collections.Concurrent;
@@ -52,7 +53,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\{screenshot}");
//
var result = AutoArtifactSalvageTask.GetArtifactGridItems(mat);
var result = GridScreen.GridEnumerator.GetGridItems(mat);
//
Assert.Equal(4, result.Count());
@@ -69,7 +70,7 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoArtifactSalvageTests
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\ArtifactGrid.png");
//
var result = AutoArtifactSalvageTask.GetArtifactGridItems(mat);
var result = GridScreen.GridEnumerator.GetGridItems(mat);
using Mat leftTopOne = new Mat(mat, result.Single(r => r.X + r.Width / 2 < mat.Width / 2 && r.Y + r.Height / 2 < mat.Height / 2));
using Mat rightTopOne = new Mat(mat, result.Single(r => r.X + r.Width / 2 > mat.Width / 2 && r.Y + r.Height / 2 < mat.Height / 2));
using Mat leftBottomOne = new Mat(mat, result.Single(r => r.X + r.Width / 2 < mat.Width / 2 && r.Y + r.Height / 2 > mat.Height / 2));

View File

@@ -0,0 +1,392 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static TorchSharp.torch.nn;
using static TorchSharp.torch;
using TorchSharp;
using System.Diagnostics;
using System.Collections;
using BetterGenshinImpact.GameTask.AutoFishing;
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
{
public partial class RodNetTests
{
/// <summary>
/// RodNet验证应在数据集上达到一定准确率
/// </summary>
[Theory]
[InlineData(@"..\..\..\Assets\AutoFishing\data_selected.csv")]
public void Training_AccuracyShouldBeOK(string dataLocation)
{
//
using var _ = no_grad();
var device =
torch.cuda.is_available() ? torch.CUDA :
torch.mps_is_available() ? torch.MPS :
torch.CPU;
var loss = CrossEntropyLoss();
var sut = new RodNet().to((Device)device);
sut.SetWeightsManually();
using var test_reader = new CSVReader(Enumerable.Repeat(false, 8).Concat(Enumerable.Repeat(true, 2)), Path.GetFullPath(dataLocation), (Device)device);
//
var accuracy = evaluate(test_reader.GetBatches(eval_batch_size), sut, loss);
//
Assert.True(accuracy > 0.8);
}
/// <summary>
/// RodNet必须粗略地支持训练
/// </summary>
[Fact]
public void Training_ShouldBeDifferentiable()
{
//
RodInput input = new RodInput();
var (y0, z0, t, u, v, h) = RodNet.GetRodStatePreProcess(input);
Tensor fishLabel = tensor(new double[] { input.fish_label }, dtype: ScalarType.Int32);
Tensor uv = tensor(new double[,] { { u, v } }, dtype: ScalarType.Float64);
Tensor y0z0t = tensor(new double[,] { { y0, z0, t } }, dtype: ScalarType.Float64);
Tensor h_ = tensor(new double[,] { { h } }, dtype: ScalarType.Float64);
RodNet sut = new RodNet();
//
Tensor output = sut.forward(fishLabel, uv, y0z0t, h_);
output.backward([torch.ones_like(output)]);
//
}
#region
// 这部分代码改编自TorchSharpExamples的CSharpExamples.TextClassification
private const long batch_size = 32;
private const long eval_batch_size = 32;
internal static RodNet Run(int epochs, int timeout, string dataLocation)
{
torch.random.manual_seed(1);
var device =
torch.cuda.is_available() ? torch.CUDA :
torch.mps_is_available() ? torch.MPS :
torch.CPU;
Console.WriteLine();
Console.WriteLine($"\tRunning TextClassification on {device.type.ToString()} for {epochs} epochs, terminating after {TimeSpan.FromSeconds(timeout)}.");
Console.WriteLine();
Console.WriteLine($"\tPreparing training and test data...");
using (var reader = new CSVReader(Enumerable.Repeat(true, 8).Concat(Enumerable.Repeat(false, 2)), dataLocation, (Device)device))
{
Console.WriteLine($"\tCreating the model...");
Console.WriteLine();
var model = new RodNet().to((Device)device);
var loss = CrossEntropyLoss();
var lr = 1e-2;
var optimizer = torch.optim.SGD(model.parameters(), learningRate: lr);
var scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, epochs, eta_min: 0);
var totalTime = new Stopwatch();
totalTime.Start();
foreach (var epoch in Enumerable.Range(1, epochs))
{
var sw = new Stopwatch();
sw.Start();
train(epoch, reader.GetBatches(batch_size), model, loss, optimizer);
sw.Stop();
Console.WriteLine($"\nEnd of epoch: {epoch} | lr: {optimizer.ParamGroups.First().LearningRate:0.0000} | time: {sw.Elapsed.TotalSeconds:0.0}s\n");
scheduler.step();
if (totalTime.Elapsed.TotalSeconds > timeout) break;
}
totalTime.Stop();
using (var test_reader = new CSVReader(Enumerable.Repeat(false, 8).Concat(Enumerable.Repeat(true, 2)), dataLocation, (Device)device))
{
var sw = new Stopwatch();
sw.Start();
var accuracy = evaluate(test_reader.GetBatches(eval_batch_size), model, loss);
sw.Stop();
Console.WriteLine($"\nEnd of training: test accuracy: {accuracy:0.00} | eval time: {sw.Elapsed.TotalSeconds:0.0}s\n");
scheduler.step();
}
foreach (var (name, param) in model.named_parameters())
{
switch (param.dtype)
{
case ScalarType.Int64:
Console.WriteLine($"参数{name}={String.Join(", ", param.data<long>())}");
break;
case ScalarType.Float32:
Console.WriteLine($"参数{name}={String.Join(", ", param.data<float>())}");
break;
case ScalarType.Float64:
Console.WriteLine($"参数{name}={String.Join(", ", param.data<double>())}");
break;
}
}
return model;
}
}
static void train(int epoch, IEnumerable<(Tensor, Tensor, Tensor, Tensor, Tensor)> train_data, RodNet model, Loss<Tensor, Tensor, Tensor> criterion, torch.optim.Optimizer optimizer)
{
model.train();
double total_acc = 0.0;
long total_count = 0;
long log_interval = 1;
var batch = 0;
var batch_count = train_data.Count();
using (var d = torch.NewDisposeScope())
{
foreach (var (y0z0t, uv, h, fish_label, success) in train_data)
{
optimizer.zero_grad();
using (var predicted_labels = model.forward(fish_label, uv, y0z0t, h))
{
var loss = criterion.forward(predicted_labels, success.to(ScalarType.Int64));
loss.backward();
torch.nn.utils.clip_grad_norm_(model.parameters(), 0.1);
optimizer.step();
total_acc += (predicted_labels.argmax(1) == success).sum().to(torch.CPU).item<long>();
total_count += success.size(0);
}
batch += 1;
if (batch % log_interval == 0)
{
var accuracy = total_acc / total_count;
Console.WriteLine($"epoch: {epoch} | batch: {batch} / {batch_count} | accuracy: {accuracy:0.00}");
}
}
}
}
static double evaluate(IEnumerable<(Tensor, Tensor, Tensor, Tensor, Tensor)> test_data, RodNet model, Loss<Tensor, Tensor, Tensor> criterion)
{
model.eval();
double total_acc = 0.0;
long total_count = 0;
using (var d = torch.NewDisposeScope())
{
foreach (var (y0z0t, uv, h, fish_label, success) in test_data)
{
using (var predicted_labels = model.forward(fish_label, uv, y0z0t, h))
{
var loss = criterion.forward(predicted_labels, success.to(ScalarType.Int64));
total_acc += (predicted_labels.argmax(1) == success).sum().to(torch.CPU).item<long>();
total_count += success.size(0);
}
}
return total_acc / total_count;
}
}
#endregion
}
internal class CSVReader : IDisposable
{
/// <summary>
///
/// </summary>
/// <param name="takeMask">按长度分组布尔值代表每组内此序号元素是否被读取。例如8个true2个false就是将近80%进入训练集</param>
/// <param name="path"></param>
/// <param name="device"></param>
public CSVReader(IEnumerable<bool> takeMask, string path, Device device)
{
this.takeMask = takeMask.ToArray();
_path = path;
_device = device;
}
private readonly bool[] takeMask;
private readonly string _path;
private readonly Device _device;
public IEnumerable<Data> Enumerate()
{
var all = File.ReadLines(_path).Skip(1); // 跳过首行列名
int count = takeMask.Length;
int maskCount = all.Count() / count;
for (int i = 0; i < maskCount + 1; i++)
{
int lastGroupCount = (i == maskCount) ? (all.Count() % count) : count;
for (int j = 0; j < lastGroupCount; j++)
{
if (takeMask[j])
{
yield return ParseLine(all.Skip(i * count + j).First());
}
}
}
}
public IEnumerable<(Tensor, Tensor, Tensor, Tensor, Tensor)> GetBatches(long batch_size)
{
// This data set fits in memory, so we will simply load it all and cache it between epochs.
var inputs = new List<Data>();
if (_data == null)
{
_data = new List<(Tensor, Tensor, Tensor, Tensor, Tensor)>();
var counter = 0;
var lines = Enumerate().ToList();
var left = lines.Count;
foreach (var line in lines)
{
inputs.Add(line);
left -= 1;
if (++counter == batch_size || left == 0)
{
_data.Add(Batchifier(inputs));
inputs.Clear();
counter = 0;
}
}
}
return _data;
}
private List<(Tensor, Tensor, Tensor, Tensor, Tensor)> _data;
private bool disposedValue;
/// <summary>
/// 将csv中的数据进行初步转换
/// </summary>
/// <param name="input"></param>
/// <returns>y0z0t、uv、h、fish_label、success张量</returns>
private (Tensor, Tensor, Tensor, Tensor, Tensor) Batchifier(IEnumerable<Data> input)
{
var y0List = new List<double>();
var z0List = new List<double>();
var tList = new List<double>();
var uList = new List<double>();
var vList = new List<double>();
var hList = new List<double>();
var labelList = new List<int>();
var successList = new List<int>();
foreach (var line in input)
{
int fish_label = line.fish_label;
int success = line.success;
RodInput rodInput = new RodInput()
{
rod_x1 = line.rod_x1,
rod_x2 = line.rod_x2,
rod_y1 = line.rod_y1,
rod_y2 = line.rod_y2,
fish_x1 = line.fish_x1,
fish_x2 = line.fish_x2,
fish_y1 = line.fish_y1,
fish_y2 = line.fish_y2
};
var (y0, z0, t, u, v, h) = RodNet.GetRodStatePreProcess(rodInput);
y0List.Add(y0);
z0List.Add(z0);
tList.Add(t);
uList.Add(u);
vList.Add(v);
hList.Add(h);
labelList.Add(fish_label);
successList.Add(success);
}
Tensor y0Tensor = tensor(y0List, dtype: ScalarType.Float64).to(_device);
Tensor z0Tensor = tensor(z0List, dtype: ScalarType.Float64).to(_device);
Tensor tTensor = tensor(tList, dtype: ScalarType.Float64).to(_device);
Tensor uTensor = tensor(uList, dtype: ScalarType.Float64).to(_device);
Tensor vTensor = tensor(vList, dtype: ScalarType.Float64).to(_device);
Tensor hTensor = tensor(hList, dtype: ScalarType.Float64).to(_device);
Tensor fish_labelTensor = tensor(labelList, dtype: ScalarType.Int32).to(_device);
Tensor successTensor = tensor(successList, dtype: ScalarType.Int32).to(_device);
return (torch.stack([y0Tensor, z0Tensor, tTensor], dim: 1), torch.stack([uTensor, vTensor], dim: 1), hTensor.unsqueeze(1), fish_labelTensor, successTensor);
}
/// <summary>
/// csv的列定义默认为time,bite_time,rod_x1,rod_x2,rod_y1,rod_y2,fish_x1,fish_x2,fish_y1,fish_y2,fish_label,success
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
public Data ParseLine(string line)
{
var columns = line.Split(",").ToArray();
return new Data(float.Parse(columns[2]), float.Parse(columns[3]), float.Parse(columns[4]), float.Parse(columns[5]),
float.Parse(columns[6]), float.Parse(columns[7]), float.Parse(columns[8]), float.Parse(columns[9]),
int.Parse(columns[10]), int.Parse(columns[11]));
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing && _data != null)
{
foreach (var (y0z0t, uv, h, label, success) in _data)
{
y0z0t.Dispose();
uv.Dispose();
h.Dispose();
label.Dispose();
success.Dispose();
}
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
internal record Data(float rod_x1, float rod_x2, float rod_y1, float rod_y2, float fish_x1, float fish_x2, float fish_y1, float fish_y2, int fish_label, int success);
}

View File

@@ -1,17 +1,16 @@
using BetterGenshinImpact.GameTask.AutoFishing;
using BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static BetterGenshinImpact.GameTask.AutoFishing.RodNet;
using TorchSharp;
using static TorchSharp.torch;
namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
{
[Collection("Init Collection")]
public class RodNetTests
public partial class RodNetTests
{
public RodNetTests(TorchFixture torch)
{
@@ -42,9 +41,8 @@ namespace BetterGenshinImpact.UnitTest.GameTaskTests.AutoFishingTests
RodNet sut = new RodNet();
//
NetInput netInput = GeometryProcessing(rodInput) ?? throw new NullReferenceException();
Tensor outputTensor = sut.ComputeScores_Torch(netInput);
double[] pred = ComputeScores(netInput);
Tensor outputTensor = sut.ComputeScores_Torch(rodInput);
double[] pred = RodNet.ComputeScores(rodInput);
//
Assert.Equal((float)pred[0], (float)outputTensor.data<double>()[0]); // 对比时降低精度,差不多就行

View File

@@ -0,0 +1,39 @@
using GameTask.Model.GameUI;
using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterGenshinImpact.UnitTest.GameTaskTests.Model.GameUI
{
public class GridScreenTests
{
[Fact]
/// <summary>
/// 测试判断前后两张图是否属于滚动,结果应正确
/// </summary>
public void IsScrolling_ResultShouldBeRight()
{
//
using Mat mat = new Mat(@$"..\..\..\Assets\AutoArtifactSalvage\ArtifactGrid.png", flags: ImreadModes.Grayscale);
using Mat cropped = mat[new Rect(0, 0, mat.Width, mat.Height - 10)];
using Mat black = new Mat(mat.Size(), mat.Type(), Scalar.Black);
using Mat scrolled = black.Clone(); // 一个向下平移了10像素的图
using Mat pos = scrolled[new Rect(0, 10, mat.Width, mat.Height - 10)];
cropped.CopyTo(pos);
//
bool result1 = GridScreen.GridEnumerator.IsScrolling(mat, scrolled, out Point2d shift);
bool result2 = GridScreen.GridEnumerator.IsScrolling(mat, mat, out Point2d _);
bool result3 = GridScreen.GridEnumerator.IsScrolling(mat, black, out Point2d _);
//
Assert.True(result1);
Assert.True(shift.Y <= 10 && shift.Y > 9.9);
Assert.False(result2);
Assert.False(result3);
}
}
}