mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-21 09:45:48 +08:00
Merge branch 'main' into d-v3
This commit is contained in:
68
.github/workflows/mirrorchyan_uploading.yml
vendored
68
.github/workflows/mirrorchyan_uploading.yml
vendored
@@ -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 }}
|
||||
|
||||
|
||||
75
.github/workflows/publish.yml
vendored
75
.github/workflows/publish.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Binary file not shown.
693
BetterGenshinImpact/Assets/Model/LICENSE
Normal file
693
BetterGenshinImpact/Assets/Model/LICENSE
Normal 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>.
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 HH:mm:ss:ffff")}.png", countArea.SrcGreyMat);
|
||||
var count = OcrFactory.Paddle.OcrWithoutDetector(countArea.CacheGreyMat);
|
||||
condensedResinCount = StringUtils.TryParseInt(count);
|
||||
// 找到树脂名称对应的按键,关键词为使用,是同一行的(高度相交)
|
||||
var useList = regionList.Where(t => t.Text.Contains("使用")).ToList();
|
||||
if (useList.Count != 0)
|
||||
{
|
||||
// 找到使用按键
|
||||
var useKey = useList.FirstOrDefault(t => t.X > TaskContext.Instance().SystemInfo.ScaleMax1080PCaptureRect.Width / 2
|
||||
&& IsHeightOverlap(t, resinKey));
|
||||
if (useKey != null)
|
||||
{
|
||||
// 点击使用
|
||||
useKey.Click();
|
||||
// 解决水龙王按下左键后没松开,然后后续点击按下就没反应了。使用双击
|
||||
Sleep(60, _ct);
|
||||
useKey.Click();
|
||||
Logger.LogInformation("自动秘境:使用 {ResinName}", resinName);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("自动秘境:未找到 {ResinName} 的使用按键", resinName);
|
||||
}
|
||||
}
|
||||
|
||||
// 脆弱树脂
|
||||
var fragileResinCountRa = ra.Find(AutoFightAssets.Instance.FragileResinCountRa);
|
||||
if (!fragileResinCountRa.IsEmpty())
|
||||
{
|
||||
// 图像右侧就是脆弱树脂数量
|
||||
var countArea = ra.DeriveCrop(fragileResinCountRa.X + fragileResinCountRa.Width, fragileResinCountRa.Y,
|
||||
(int)(fragileResinCountRa.Width * 3), fragileResinCountRa.Height);
|
||||
var count = OcrFactory.Paddle.Ocr(countArea.SrcMat);
|
||||
fragileResinCount = StringUtils.TryParseInt(count);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInformation("剩余:浓缩树脂 {CondensedResinCount} 原粹树脂 {FragileResinCount}", condensedResinCount,
|
||||
fragileResinCount);
|
||||
return (condensedResinCount, fragileResinCount);
|
||||
/// <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()
|
||||
|
||||
73
BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs
Normal file
73
BetterGenshinImpact/GameTask/AutoDomain/Model/ResinStatus.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -96,6 +96,11 @@ public partial class AutoFightConfig : ObservableObject
|
||||
[ObservableProperty]
|
||||
private int _pickDropsAfterFightSeconds = 15;
|
||||
|
||||
/// <summary>
|
||||
/// 拾取战斗人次阈值,当战斗人次小于一定次数,就结束战斗情况下,不触发拾取掉落物和万叶拾取后拾取,只有不小于2时才生效。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private int? _battleThresholdForLoot;
|
||||
/// <summary>
|
||||
/// 战斗结束后,如果存在枫原万叶,则使用该角色捡材料
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("距最近一次万叶出招,时间过短,跳过此次万叶拾取!");
|
||||
}
|
||||
}
|
||||
//切换过队伍的,需要再切回来
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing;
|
||||
using static TorchSharp.torch;
|
||||
|
||||
namespace BetterGenshinImpact.GameTask.AutoFishing;
|
||||
|
||||
public record RodInput
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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": "其他武器",
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
232
BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs
Normal file
232
BetterGenshinImpact/GameTask/Model/GameUI/GridScreen.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}"));
|
||||
}
|
||||
}
|
||||
410
BetterGenshinImpact/View/Controls/Drawer/CustomDrawer.cs
Normal file
410
BetterGenshinImpact/View/Controls/Drawer/CustomDrawer.cs
Normal 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
|
||||
}
|
||||
27
BetterGenshinImpact/View/Controls/Drawer/DrawerStyles.xaml
Normal file
27
BetterGenshinImpact/View/Controls/Drawer/DrawerStyles.xaml
Normal 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>
|
||||
71
BetterGenshinImpact/View/Controls/Drawer/DrawerViewModel.cs
Normal file
71
BetterGenshinImpact/View/Controls/Drawer/DrawerViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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="" 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="" 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
86
BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml
Normal file
86
BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml
Normal 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>
|
||||
126
BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs
Normal file
126
BetterGenshinImpact/View/Windows/ScriptRepoWindow.xaml.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +149,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel
|
||||
}
|
||||
|
||||
// 更新仓库
|
||||
ScriptRepoUpdater.Instance.AutoUpdate();
|
||||
// ScriptRepoUpdater.Instance.AutoUpdate();
|
||||
|
||||
// 清理临时目录
|
||||
TempManager.CleanUp();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,6 @@ public partial class KeyMouseRecordPageViewModel : ViewModel
|
||||
public void OnOpenLocalScriptRepo()
|
||||
{
|
||||
Config.ScriptConfig.ScriptRepoHintDotVisible = false;
|
||||
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
|
||||
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -79,7 +79,7 @@ public partial class AutoFightViewModel : ObservableObject, IViewModel
|
||||
public void OnOpenLocalScriptRepo()
|
||||
{
|
||||
Config.ScriptConfig.ScriptRepoHintDotVisible = false;
|
||||
ScriptRepoUpdater.Instance.OpenLocalRepoInWebView();
|
||||
ScriptRepoUpdater.Instance.OpenScriptRepoWindow();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]); // 对比时降低精度,差不多就行
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user