update # 初始化仓库

This commit is contained in:
2026-10-15 14:14:58 +08:00
commit ff03600ae9
859 changed files with 17559 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
trim_trailing_whitespace = true
[*.py]
indent_size = 4
max_line_length = 80
[*.{json,yml,yaml,toml}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[legacy/**]
trim_trailing_whitespace = false
insert_final_newline = unset
end_of_line = unset
+19
View File
@@ -0,0 +1,19 @@
* text=auto
*.py text eol=lf
*.md text eol=lf
*.json text eol=lf
*.toml text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.html text eol=lf
*.sh text eol=lf
*.ps1 text eol=lf
*.png binary
*.jpg binary
*.jpeg binary
*.db binary
*.woff binary
*.woff2 binary
# Preserve exact legacy bytes so manifest hashes survive checkout
legacy/** -text
+113
View File
@@ -0,0 +1,113 @@
# Python bytecode, native extensions and interpreter caches
__pycache__/
*.py[cod]
*$py.class
*.so
*.pyd
# Virtual environments and local Python version selection
.venv/
venv/
env/
ENV/
.python-version
# Build and packaging outputs
/build/
/dist/
/wheelhouse/
*.egg-info/
.eggs/
*.egg
pip-wheel-metadata/
# Test, lint, typing, coverage and profiling outputs
.pytest_cache/
.ruff_cache/
.mypy_cache/
.pyright/
.tox/
.nox/
.hypothesis/
.coverage
.coverage.*
htmlcov/
coverage.xml
*.prof
*.lprof
# Local configuration and credentials; keep distributable templates
.env
.env.*
!.env.example
!.env.*.example
*.pem
*.key
/secrets/
/config.local.*
/settings.local.*
# Editors, operating system and temporary files
.idea/
.vscode/
.DS_Store
Thumbs.db
Desktop.ini
*.swp
*.swo
*~
*.tmp
*.part
# Farm runtime state: never version player data or generated images
/data/
/runtime/
/cache/
/tmp/
/temp/
/logs/
/backups/
/exports/
/downloads/
/uploads/
/artifacts/
/.local/
**/farm_db/
**/farm_res/
*.log
*.log.*
*.db
*.sqlite
*.sqlite3
*.db-journal
*.db-wal
*.db-shm
*.sqlite-journal
*.sqlite-wal
*.sqlite-shm
*.sqlite3-journal
*.sqlite3-wal
*.sqlite3-shm
*.bak
*.backup
# Exact allowlist: bundled static crop catalogues, not player databases
!/legacy/zhenxun_plugin_farm/resource/db/plant.db
!/legacy/zhenxun_plugin_farm/resource/db/plant-test.db
# Legacy downloads and generated render output
/legacy/zhenxun_plugin_farm/config/sign_in.json
/legacy/zhenxun_plugin_farm/config/signTemp.json
/legacy/zhenxun_plugin_farm/resource/html/*.png
# Local publication checkouts; not submodules or source directories
/.release-workspaces/
/.release-staging/
/.release-cache/
# Local agent installation and runtime state; AGENTS.md remains tracked
/.agents/
/.codex/
/.codebase-memory/
/.comet/*
!/.comet/config.yaml
+31
View File
@@ -0,0 +1,31 @@
# Farm 协作约定
- 面向用户的回复及设计文档使用中文;区分事实、建议、待确认和已完成修改。
- 本仓库是唯一开发源;三个框架发布仓库由未来流水线生成,不直接维护业务副本。
- 检查、审查、规划默认只读;实现需要明确授权。
- legacy/zhenxun_plugin_farm 为迁移参考基线。除已记录的凭据移除,不修改基线业务;修复应进入新实现并关联审查编号。
- 新代码使用 Python,4 空格缩进,行长 80;枚举、数据结构及字段说明保持完整。
- 核心不得导入 nonebot、astrbot、zhenxun;框架对象、用户事件和聊天消息止于接入层。
- 数据库保存权威业务状态;交易由业务服务定义边界,底层方法不得自行提交上层事务。
- 数量和内部货币使用整数;禁止将累计值传给增量接口;不足应失败,不静默归零。
- 关键交易记录结构化日志与流水;不记录令牌、凭据、完整事件或真实玩家数据。
- 使用本地文件工具检查源码;结构查询优先代码图,缺失或过期时核对源文件。
- legacy-manifest.json 用于核对迁移完整性;更新基线必须记录差异和来源。
- .release-workspaces 不进入主仓库。本轮用户要求停止拉取,已删除本地克隆;不得自行重新克隆、fetch、pull 或推送。
- docs 中的框架接入和发布流水线属于设计,未实现部分不得报告为已完成。
## Comet Ambient Resume
在这个仓库中,开始处理需要改动或调查的任务前,如果可能存在活跃 Comet workflow,把当前用户请求传入只读探针:`comet resume-probe . --stdin --json`。
- 如果用户通过宿主明确调用任意 Comet Skill(例如 `@comet`、`/comet`、`@comet-native` 或 `/comet-hotfix`),显式调用优先于本恢复协议;不要运行 resume probe,直接进入被调用的 Skill。
- 如果用户通过宿主明确调用的是非 Comet 的 Skill 或斜杠命令,任务意图已由该调用明确:不要运行 resume probe,直接执行该 Skill。
- 如果你正在 Comet 流程内(包括正在等待用户回复你在流程中提出的问题),不要运行 resume probe;把这类回复(例如方案/选项选择)当作当前 change 的继续,直接按用户的选择推进。
- 只信任返回的 `workflow`、`skill` 和 `entrySource`;它们只由项目配置或无配置兼容回退决定。不得扫描或切换另一套 workflow。
- 如果 probe 返回 `auto_resume`,简短说明选中的 active change,并进入 `nextCommand` 指向的永久入口。不要把状态命令当作恢复入口直接推进。
- 如果 probe 返回 `ask_user`,只问一个简短问题并等待用户回复。
- 如果当前请求未明确调用 Comet Skill,且 probe 返回 `out_of_scope` 或 `none`,不要进入 Comet workflow。
- `out_of_scope` 或 `none` 只表示不要因为这个新请求进入 Comet workflow;它绝不表示要暂停或退出一个已在进行的 Comet 流程。
- 如果配置或状态无效且没有 `nextCommand`,停止并报告原因;不要猜测另一个 workflow。
- 不能只因为存在 active change 就把无关任务挂到该 change。Native 的未提交改动由 Native 入口检查,不由探针自动归因。
</comet-ambient-resume>
+674
View File
@@ -0,0 +1,674 @@
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:
<program> Copyright (C) <year> <name of author>
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>.
+40
View File
@@ -0,0 +1,40 @@
# Farm
一个源码主仓库、多框架发布仓库的农场插件翻新项目。
当前阶段:初始化、旧代码保全与设计评审。新的跨框架农场尚未实现,
本目录不能直接作为 NoneBot、AstrBot 或真寻插件安装。
## 当前已有内容
- docs/:长期规划、架构、业务约束、发布设计、迁移与源码审查。
- legacy/zhenxun_plugin_farm/:旧 Beta 分支代码与资源的迁移基线。
- src/、integrations/、tests/、release/:职责说明和未来实施位置。
- scripts/check_baseline.py:离线校验迁移清单、Python 语法及静态数据库。
- Git 已本地初始化为 main;没有配置主仓库远程,也未提交或推送。
先读 [文档索引](docs/README.md),再读
[待确认决策](docs/07-decisions.md)。
## 校验
使用 Python 3.10 或更高版本:
```text
python scripts/check_baseline.py
git status --short
```
脚本不启动机器人,不执行网络请求,不读真实玩家数据库。
语法通过不等于插件加载或玩法正确。
## 来源与许可证
旧代码来源为 Shu-Ying/zhenxun_plugin_farm 的 Beta 分支,
提交 b015c6c1997f146f651b677382980d338a306bb2。
保留原 GPLv3 LICENSE、原作者信息和旧 README。
素材权属尚需逐项核对,不因代码许可证而自动完成素材授权判断。
同步副本中的服务令牌已改为读取 FARM_SERVICE_TOKEN 环境变量,
其余源码与资源复制差异见 docs/legacy-manifest.json。
不要直接部署含已知业务缺陷的迁移基线。
+46
View File
@@ -0,0 +1,46 @@
# 长期规划
## 已确认目标
一个 farm 主仓库统一开发农场逻辑;面向真寻、普通 NoneBot 和
AstrBot 生成不同插件;三个发布仓库只承载各自的安装内容。
用户不需要下载其他框架的接入代码。
保留旧项目功能作为需求调查基础,不默认旧实现正确,也不默认旧
README 中所有功能都要无条件保留。先修数据一致性,再增加玩法。
## 建议的首个版本范围
保留开通农场、查看、种子商店、购买、播种、收获、铲除、出售、
锁定、土地升级、签到、偷菜、改名。逐项核对旧指令与返回行为。
真寻金币兑换为真寻专有可选能力;其他框架不凭空生成外部金币。
点券、远程签到与星级规则在决策确认后进入规格。
首版不做交易行、钓鱼、跨实例在线联机、账号自动绑定、Web 后台、
通用插件引擎、复杂事件总线和微服务。后续依据需求增加。
## 里程碑和退出条件
| 阶段 | 工作 | 完成标准 |
|---|---|---|
| M0 初始化 | 代码保全、审查、设计和忽略规则 | 哈希可验证;实现与计划状态清楚 |
| M1 规格收敛 | 确定身份、共享范围、经济与迁移策略 | 决策表关键项已确认,行为示例可验收 |
| M2 核心与存储 | 注册、货币、库存、迁移及事务 | 无框架依赖;失败回滚、重试幂等、并发余额测试通过 |
| M3 种植闭环 | 购买→播种→收获→出售、土地升级 | 边界、星级、锁定、经验计算及时间测试通过 |
| M4 真寻接入 | 保留既有用户入口,迁移旧数据 | 真实宿主的安装、启停、文本/图片与恢复演练通过 |
| M5 其他接入 | 普通 NoneBot、AstrBot | 每个宿主独立安装;无其他框架依赖;功能矩阵通过 |
| M6 发布链路 | 分包、来源清单、发布仓库同步 | ZIP/wheel 安装测试、凭据与体积检查、可重试发布通过 |
| M7 稳定运营 | 性能、备份、资源供应与升级 | 有容量指标、兼容矩阵和版本回滚演练 |
| M8 可选联机 | 独立服务、账号映射、共享状态 | 另立需求,不直接共用多个进程的 SQLite 文件 |
阶段顺序表达依赖,不作未经验证的工期承诺。M4/M5 先打通一个最小
指令与完整交易闭环,再扩展全部指令;不要先写完所有框架外壳。
## 质量门槛
- 新玩法先给数值和边界例子;保留用户接受的契约。
- 每次修改聚焦一个可验收用例,避免同时换数据库、框架和玩法。
- 测试以交易不变量、并发、迁移和真实安装为主,不只检查函数返回。
- 核心测试不能依赖机器人、外网或图片渲染器。
- 每次发布追溯到主仓库提交、目标、资源版本、schema 和依赖清单。
- 所有目标达到对应门槛才能称为“支持”;未实现目标保持禁用。
+110
View File
@@ -0,0 +1,110 @@
# 代码设计与目录
## 原则与当前状态
下列为建议设计,尚未实现。当前旧代码位于 legacy,未把其全局变量、
数据库管理器和服务定位器直接当作新核心继续复用。
采用一个共享 Python 包,内部按职责划分,避免先建多个互相依赖的
独立发行包。共享源码只维护一份;发布时复制进各插件私有目录。
## 目标结构
```text
farm/
src/farm_core/
domain/ # 规则、值对象、状态转换,不做 I/O
application/ # 注册/购买/播种等用例,拥有事务边界
ports/ # 必要的仓储事务、时钟、外部钱包契约
infrastructure/
sqlite/ # 连接、仓储、schema migration
resources/ # 作物目录、资源校验与更新
presentation/ # 框架无关展示数据;可选图片渲染
integrations/
zhenxun/ # 真寻命令、配置、金币、数据目录、生命周期
nonebot/ # 普通 NoneBot 插件,不导入真寻
astrbot/ # AstrBot 插件,不导入 NoneBot 或真寻
assets/ # 经整理、版本化的资源,尚未从 legacy 提取
tests/
unit/ integration/ contracts/ migrations/ packaging/
release/ # 构建目标、文件白名单、依赖与模板
scripts/ # 离线校验、迁移、未来打包工具
legacy/ # 旧实现参考;不进入新插件安装包
docs/
```
本轮只创建职责说明及迁移基线,不生成大量空模块或假实现。
当一个用例实施时,才创建所需模块。
## 依赖方向
```mermaid
flowchart TD
Z[真寻接入] --> A[应用用例]
N[NoneBot 接入] --> A
B[AstrBot 接入] --> A
A --> D[领域规则]
A --> P[必要接口]
S[SQLite 实现] --> P
R[资源实现] --> P
Z --> C[组装与生命周期]
N --> C
B --> C
C --> S
C --> R
A --> V[展示数据]
V --> I[可选图片渲染]
```
基础设施实现接口,由宿主启动阶段组装并注入;领域与应用代码不能
访问框架全局对象、事件、配置管理器或消息发送 API。
记录日志使用标准 logging,接入层按需桥接宿主日志。
## 每层的责任
| 层 | 应做 | 禁止混入 |
|---|---|---|
| 命令接入 | 参数解析、用户上下文、权限、确认、发送回复 | 再写一套价格/奖励计算 |
| 应用用例 | 业务校验、幂等、事务、调用领域计算 | 对聊天平台对象直接操作 |
| 领域规则 | 土地/星级/经验计算与状态转换 | SQL、网络、图片、框架导入 |
| 存储 | 条件更新、查询、schema 升级 | 命令文案、签到奖励选择、图片 |
| 展示 | 将结果渲染为文本/图片 | 修改余额和农场状态 |
| 资源 | 校验、版本、缓存与原子切换 | 未验证即覆盖线上目录 |
命令与业务服务返回明确结果,例如成功、余额不足、种子不足、
已处理、状态冲突;结果包含展示所需数值和错误码。
数据库故障不能转换成“没有记录”或“没有农场”。
## 最小接口建议
- UnitOfWork:一次用例获取同一事务下的仓储;成功才提交。
- Clock:提供带时区的当前时间,使跨日、成熟和签到可测试。
- PlantCatalog:提供带版本的作物定义,已种植实例绑定规则版本。
- ExternalWallet:仅在真寻金币等外部经济接入时启用,明确幂等与补偿。
不要为字符串工具、日志和每个函数都建接口。
领域对象使用明确数据类型,取代任意 dict[Any, Any]。
注册、库存、地块、货币、收获周期分别建模,枚举及字段有说明。
## 生命周期和异步模型
导入模块时不启动清理线程、不连接数据库、不发送网络请求。
宿主启动:验证配置→迁移→初始化仓储/资源→就绪;
宿主停止:拒绝新写入→等待在途用例→关闭连接和任务。
首期建议单进程嵌入式部署。不要用 threading.RLock 代替异步业务锁。
事务连接生命周期由 UnitOfWork 管理;即使使用进程内锁,SQL 仍需
条件更新和唯一约束。查询缓存首期可不做;需要时在提交后失效,
不缓存不存在的用户作为有效玩家,不把缓存余额作为扣款依据。
## 发布时的包隔离
src/farm_core 内部一律采用相对导入。
构建时复制为 <插件包>/_vendor/farm_core,接入层经包内路径导入。
_vendor 必须是普通包且顶层初始化无副作用。
禁止把共享核心作为全局同名 farm_core 安装后又在插件中带另一份,
避免同时安装真寻/NoneBot 包时导入碰撞。
第三方依赖只列核心必需项和当前接入层项;渲染依赖可选。
私有命名空间能隔离源码名称,不能隔离同一 Python 进程的第三方依赖
版本;冲突必须通过兼容测试或分进程解决。
+101
View File
@@ -0,0 +1,101 @@
# 业务逻辑与一致性设计
状态:以下是建议约束。涉及用户资产归属、玩法与跨实例互通的选择,
需按 07-decisions.md 确认后进入正式规格。
## 身份与农场归属
框架(真寻/NoneBot/AstrBot)不等于消息平台(QQ/Telegram 等)。
玩家至少需要 deployment_id、realm_id、platform、platform_user_id;
platform_user_id 保存字符串,禁止跨平台按相同数字自动认定同一人。
建议独立部署默认独立数据,realm 可配置群内或部署内共享。
各协议暴露的同平台 ID 也未必同义,官方 QQ 的开放标识与 OneBot
账号不能自动合并。迁移需要明确来源命名空间、人工确认映射与审计。
双人操作如偷菜必须确定双方处于同一允许交互的 realm,
检查自己偷自己、跨群目标、无农场目标和权限。@对象解析留在接入层。
## 货币与库存
- 内部货币、数量使用整数;设置合理上限并检查乘法后的范围。
- 增量和设置新值采用不同接口名称;应用层不能直接写余额缓存。
- 扣款使用数据库条件更新,例如余额足够才扣减,检查实际影响行数。
- 不足返回明确失败;不能静默把负余额截成零后继续发货。
- 每次资产变动记录 operation_id、actor、realm、币种、增量、原因、
关联物品和提交时间;日志不代替可查询的账本。
- 已锁定作物在单项出售、批量出售、交易与其他消耗入口采用一致规则。
- 所有数量入口测试 0、负数、缺省、超大值、全部数量与库存不足;
命令层的默认参数不得被误当成任意合法业务值。
## 购买与出售
购买:读取有效目录→校验数量/等级→同一事务中校验和扣款→增加种子
→写操作流水→提交→生成展示数据。任一步失败整体回滚。
出售:按明确数量计算可出售库存→同一事务中检查锁定和库存版本
→扣库存→只增加本次收益→流水→提交。返回实际成交数量与新余额。
网络发送失败不回滚已成交交易;重试同一 operation_id 返回原结果。
## 播种与收获
播种:空地校验、种子消耗、星级选择、作物实例创建在同一事务中。
土地收益与生长速度采用已确认公式,记录规则或目录版本,
不能让资源更新悄悄改变已播种作物的结算契约。
收获:每块地独立计算产量和经验,再求和;不对累计经验反复加成。
每轮作物有可辨识的 cycle_id/harvest_index,防止重试和并发重复领奖。
作物入仓、经验增加、收获次数、下一成熟时间或枯萎状态、偷取记录
的结算必须在同一业务事务内完成。精确的事务批次由全收获与部分成功
策略决定,先定义用户可见结果,不能任意留半完成状态。
未种植、生长中、成熟、枯萎是明确状态;不要用多组互相冲突的布尔值
表示同一阶段。除草、虫害、施肥等独立属性另行建模。
## 签到、升级与确认
签到采用明确业务时区,(realm、player、business_date) 唯一;
签到记录、奖励发放、累计奖励领取标记在同一事务内。
“已签到”与“新签到成功”分开返回,防止重复展示发奖。
补签与连续签到规则分别定义,不从字段名猜测。
开垦和土地升级在用户确认后重新读取余额与地块状态,重新校验条件。
确认凭据绑定发起人、会话、动作、参数、过期时间和一次性标识;
不能把任何消息里的“是”当作任意待办操作的确认。
升级使作物成熟这一旧行为需明确保留或调整。
## 幂等与并发
operation_id 由框架事件标识与部署作用域产生;没有稳定事件标识时,
用已持久化的交互操作标识,而不是每次重试生成新的随机 ID。
同一操作不同参数应拒绝,不能错误复用第一次结果。
唯一约束、条件更新或状态版本是最终防线。
进程内锁只作辅助,跨进程不能共享这把锁。
单实例 SQLite 的连接/事务应由一个明确管理者协调;
BEGIN 的锁等待、busy timeout、取消回滚、事务嵌套和连接关闭都要测试。
WAL 是部署选项,不能解决错误的业务事务边界。
## 外部金币兑换
真寻金币和本地农场币位于不同存储,不能承诺一个本地 SQLite 事务
同时回滚两边。先核对外部钱包是否支持 operation_id、条件扣款、
查询和退款;使用持久化兑换单与可重试状态机协调。
无法确认结果时进入待对账状态,禁止直接重复扣款或盲目发币。
普通 NoneBot、AstrBot 默认只支持内部农场货币;
外部经济系统通过能力配置接入,不导入真寻代码。
兑换比例、手续费、舍入、最小单位和失败恢复写入明确规格。
## 时间、事件与展示
保存 UTC 时间戳,业务日期按配置时区计算;日志记录时区语义。
时钟可注入,覆盖午夜、闰日、月切换和重启后的结算。
核心状态改变必须由事务内显式步骤完成。
提交后的消息、图片、统计等副作用失败不撤销已提交业务;
需要可靠重试时使用操作结果或持久化任务。事件槽捕获异常不能把
必需的资产更新失败隐藏成成功。首期不必新增通用事件总线。
展示读取业务结果,不在绘图过程中更新余额或土地;
图片失败回退文本,不重复执行业务。
+101
View File
@@ -0,0 +1,101 @@
# 一个主仓库与多个发布仓库
## 已确认的仓库关系
| 角色 | 远程地址 | 建议职责 |
|---|---|---|
| 开发主仓库 | 尚未提供;本地 D:/Work/farm | 业务、接入、测试、文档、构建的唯一开发源 |
| 真寻发布仓库 | https://github.com/Shu-Ying/zhenxun_plugin_farm.git | 真寻完整插件 |
| AstrBot 发布仓库 | https://github.com/Shu-Ying/astrbot_plugin_farm.git | AstrBot 完整插件 |
| NoneBot 发布仓库 | https://github.com/Shu-Ying/nonebot_plugin_farm.git | NoneBot 包及 PyPI 构建资料 |
三个发布仓库已存在,不是空仓库,也不是 Git 子模块。
本轮曾按用户要求完成 clone,随后用户要求停止并删除,三个本地副本
及 .release-workspaces 已删除。不再 clone/fetch/pull/push。
历史分支和提交记录保留在 repository-inventory.json,并标明已删除。
历史观察:三个远程默认分支均为 main;与源 Beta 的代码布局不同。
因此未来不能直接覆盖 main 或假设旧 Beta 就等于远程稳定版。
首次接管发布流程必须先核对各仓库现有功能、许可、兼容性和用户安装路径。
## 发布内容与目录
每个发布产物 = 同一主提交的共享核心 + 当前框架接入 + 对应依赖
+ 必需资源 + 许可证/归属 + 来源清单。
legacy、docs、tests、开发工具、其他框架入口、真实数据不进入安装包。
真寻仓库根保留其可被插件目录直接加载的布局;AstrBot 根保留
main.py、metadata.yaml、配置描述和 requirements.txt 等宿主所需文件;
NoneBot 根为 pyproject.toml 加 nonebot_plugin_farm/ 模块。
这些布局必须以真实宿主安装验证为准,不只检查文件名。
各插件私有包含 _vendor/farm_core;共享核心内部使用相对导入。
NoneBot wheel 明确包含资源;不能假设 zip 的相对目录在 wheel 内
也自动存在。分别验证仓库安装、ZIP 安装和 wheel 安装。
## 建议发布流程
1. 选定主仓库提交或受保护版本标签,计算统一版本和资源版本。
2. 对各目标执行文件白名单组装,不从开发目录无差别复制。
3. 运行核心测试、迁移、依赖边界检查及对应宿主加载/启停测试。
4. 生成 ZIP/wheel、SHA-256、文件清单、构建环境与来源信息。
5. 校验资源大小、许可证、无其他框架代码/依赖、无令牌和玩家数据。
6. 为每个发布仓库生成可审查的候选差异;首次切换先 PR,
经授权后发布。PR/推送/发布不由这份设计文档自动授权。
7. 在发布仓库内用普通提交更新产物,不 force push,不改写历史。
8. 提交安装内容和来源清单后,再创建指向同一提交的发布标签。
9. 按目标发布到仓库/Release/PyPI,并记录各目标结果。
未来可使用 GitHub Actions;本轮未编写可执行发布工作流、未配置凭据。
发布工作区使用隔离的临时目录;清理只允许精确列出的生成路径。
保留目标仓库中的治理资料和人工维护文件,禁止无条件清空仓库。
## 版本、重试与回滚
首期建议核心和插件采用统一产品版本;目标元数据附 source_commit、
target、resource_version、schema_version 和构建内容哈希。
来源清单不能嵌入“包含清单本身的整个包哈希”形成循环依赖;
先列内容哈希,最终包哈希写独立校验附件。
跨仓库更新不是一个 Git 事务。为一次发布保存发布 ID 和目标状态;
部分失败只重试相同来源的失败目标,不重新构建不同内容冒充同版本。
相同来源、版本和目标应生成相同文件内容;统一归档排序及时间元数据。
已有标签和已上传 PyPI 版本不可覆盖,用新修复版本处理。
回滚插件二进制/源码不代表可以回滚数据库。
只有 schema 向后兼容或恢复备份后才能回退;停止机器人并校验后操作。
禁止用户通过退回旧包直接打开不兼容的新数据库。
## 权限与触发
GITHUB_TOKEN 限于当前仓库场景,跨仓库同步需另设明确授权;
建议使用仅获三个目标仓库权限的 GitHub App 安装令牌。
不在配置文件内写 PAT、不把令牌放 Git URL,不在不可信 PR 中执行发布。
构建可读权限与发布写权限分开,发布使用受保护环境和固定目标白名单。
凭据由仓库 Secrets/环境提供,版本和目标必须校验。
## 资源体积与分发
本轮测得迁移基线 ZIP 为 26,084,694 字节,约 24.9 MiB;
这是尺寸探测,不是可安装发布包。
AstrBot 当前发布文档列出的市场 ZIP 上限为 16 MB,因此直接打包全量
旧资源不可行。优先审查未使用图片、无损优化和基础资源集;
如仍需外置资源,采用固定版本、HTTPS、哈希和大小上限、
临时下载验证后原子安装,提供离线资源包。
不默认开发版直连远程服务,不跟随 latest 获取未绑定版本的作物库。
必须决定首次离线可玩范围、远程资源不可用时的回退、资源包保留版本,
以及图片和字体的分发权属。资源 schema 也要跟随代码兼容矩阵。
## 官方依据
核对日期:2026-10-15;正式发布前重新验证。
- [AstrBot 发布说明](https://docs.astrbot.app/dev/star/plugin-publish.html):
仓库元数据与市场 ZIP 体积要求。
- [AstrBot 安装方式](https://docs.astrbot.app/use/plugin.html):
仓库链接或 ZIP 文件安装。
- [NoneBot 发布说明](https://nonebot.dev/docs/developer/plugin-publishing):
Python 包、PyPI 与官方商店发布路径。
- [GitHub Actions 身份验证](https://docs.github.com/en/actions/tutorials/authenticate-with-github_token):
GITHUB_TOKEN 权限与其他授权方式。
+61
View File
@@ -0,0 +1,61 @@
# 旧代码与数据迁移
## 本轮实际同步
源:D:/Work/zhenxun_plugin_farm,Beta 分支,
提交 b015c6c1997f146f651b677382980d338a306bb2。
复制工作区中受版本管理的文件,不复制 .git、未跟踪的工具安装目录。
源 .gitignore 含上轮 Comet 规则,未复制;新主仓库使用自己的忽略策略。
目的地:legacy/zhenxun_plugin_farm。827 个文件、35 个 Python 文件、
静态配置、HTML、图片以及两个仅含 plant/sqlite_sequence 表的作物库。
两库只读 quick_check 均为 ok,未复制运行中的 farm.db 或玩家备份。
静态资源是否全有合法分发授权尚未确认。
唯一源码差异:utils/request.py 的硬编码服务令牌改为读取
FARM_SERVICE_TOKEN,未把原值复制到新仓库。
来源哈希、复制哈希及差异说明见 legacy-manifest.json。
原项目未修改,服务端令牌轮换需另行处理。
## 旧模块复用地图
| 旧位置 | 新去向 | 迁移方式 |
|---|---|---|
| command.py、根 __init__.py | integrations/zhenxun | 保留用户指令契约,重写生命周期和参数适配 |
| core/farm.py、core/shop.py | application、domain | 按用例提取,先修计算和交易缺陷 |
| core/database | infrastructure/sqlite | 明确 schema 与事务所有权,不直接照搬 |
| core/player | 领域模型/查询模型 | 首期移除余额权威缓存,不移植后台线程池 |
| utils/darw、HTML | presentation/资源 | 去除真寻 BuildImage 依赖,支持文本降级 |
| utils/request、utils/json | resources | 配置校验、版本固定、下载原子性 |
| event | 提交后副作用 | 逐个核对必要用途,不先移植通用信号层 |
| core/activity/activity.py、effect.py | 后续玩法候选 | 隔离加载失败,不接入首版 |
| resource/db | 版本化作物目录 | 区分内置只读目录与可写玩家数据库 |
## 真实用户数据迁移建议
当前没有读写任何真实玩家库,以下尚未实施:
1. 盘点实际部署版本、schema、来源平台、玩家数量和货币含义。
2. 停写并备份,记录 schema、文件哈希和备份位置;备份不入 Git。
3. 使用副本进行只读预检:主键、重复、负数、孤儿地块、未知作物、
作物成熟时间、锁定、签到日期、币种和数值范围。
4. 明确旧裸 UID 到新身份命名空间的映射,不自动跨平台合并。
5. 版本化迁移脚本写入新数据库,保存 migration_id 和 schema_version。
6. 校验用户数、各币种合计、种子/作物数量、地块数及抽样业务状态,
对每一项转换或舍弃生成报告;再次执行不得重复导入。
7. 在目标框架演练最小玩法闭环、时区与资源加载,再切换数据目录。
8. 若失败,停写后恢复旧程序与原备份,不让两个版本并行写同一数据库。
旧代码存在余额和经验重复增加,无法仅凭当前余额还原“正确历史值”。
是否保留已有资产、重置、补偿或人工对账必须由用户明确决定,
不在迁移脚本中暗自纠正或清零。
## 首次发布仓库接管
当前本地 Beta 基线不等于三个发布仓库 main。
未来需要取得重新拉取授权后,对比现有功能和依赖,记录有价值的框架
接入代码与差异,再决定复用方式。不能本轮默认把远程现有代码覆盖成
Beta,也不能把已有 AstrBot/NoneBot 实现宣称已经合并进新主仓库。
发布仓库地址已纳入设计,检出已按用户要求删除。仅迁移基线保留在
主仓库,主仓库没有 Git 子模块和发布仓库副本。
+62
View File
@@ -0,0 +1,62 @@
# 源码审查
日期:2026-10-15。范围:旧 Beta 迁移基线的关键用例、数据层、
玩家缓存、初始化、资源请求和未完成的活动模块。
结论:有功能拆分基础,但交易边界、状态权威、错误传播和分层不足,
不建议直接在此结构上继续扩展跨框架玩法。
## 证据与限制
代码图提供符号和调用线索,覆盖检查报告 metadata_changed;
以下结论均回到源码核对,不以图的缺边证明代码不存在。
链接指向复制基线,通常与源行号一致;request.py 已移除令牌。
本轮语法检查和两个模块隔离加载见 baseline-checks.json。
没有启动真实宿主、执行完整插件导入、联网访问旧服务或开展并发压测。
P1:数据、资产或主要功能错误,应在新核心实施前处理。
P2:功能契约或维护可靠性问题。风险项与已证实缺陷分别标识。
## 已确认缺陷
| 编号 | 等级 | 证据 | 触发与影响 | 修复方向 |
|---|---|---|---|---|
| F01 | P1 | [shop.py:241](../legacy/zhenxun_plugin_farm/core/shop.py#L241)、[player.py:62](../legacy/zhenxun_plugin_farm/core/player/player.py#L62) | 出售把原余额加收入传给增量接口;100+20 得到 220 | 收益只加一次;交易内更新 |
| F02 | P1 | [farm.py:327](../legacy/zhenxun_plugin_farm/core/farm.py#L327)、[player.py:119](../legacy/zhenxun_plugin_farm/core/player/player.py#L119) | 收获把原经验再次传给增量接口,重复累计 | 清晰区分增量与新值 |
| F03 | P1 | [farm.py:262](../legacy/zhenxun_plugin_farm/core/farm.py#L262) | 循环中对累计经验应用当前土地加成,前面经验被再次放大 | 各地块独立计算后相加;交换地块顺序测试 |
| F04 | P1 | [user.py:102](../legacy/zhenxun_plugin_farm/core/database/user.py#L102) | updatePoint 丢弃底层失败结果,直接 True,内存可能领先落库 | 原样传播失败,提交后失效缓存 |
| F05 | P1 | [player.py:27](../legacy/zhenxun_plugin_farm/core/player/player.py#L27)、[tool.py:25](../legacy/zhenxun_plugin_farm/utils/tool.py#L25)、[command.py:53](../legacy/zhenxun_plugin_farm/command.py#L53) | 未注册玩家被缓存为空 dict,创建数据库记录后不刷新,后续判断仍未注册 | 不缓存无效玩家;注册提交后刷新 |
| F06 | P1 | [shop.py:161](../legacy/zhenxun_plugin_farm/core/shop.py#L161) | 扣款与发种分离,忽略扣款结果,发种失败不回滚 | 应用层一个事务覆盖交易 |
| F07 | P1 | [userSign.py:158](../legacy/zhenxun_plugin_farm/core/database/userSign.py#L158)、[userSign.py:229](../legacy/zhenxun_plugin_farm/core/database/userSign.py#L229) | 签到记录先提交,奖励在其后发放,中断会已签到未到账 | 唯一签到与奖励在同一事务,返回原操作结果 |
| F08 | P2 | [shop.py:194](../legacy/zhenxun_plugin_farm/core/shop.py#L194) | 只在全部出售检查锁定,指定作物分支绕开检查 | 统一可消耗库存规则 |
| F09 | P2 | [activity.py:46](../legacy/zhenxun_plugin_farm/core/activity/activity.py#L46)、[effect.py:46](../legacy/zhenxun_plugin_farm/core/activity/effect.py#L46) | 隔离加载分别因 ActivityType、Player 未定义而失败 | 标为未完成功能,不进入首版;不能仅以语法判断可用 |
| F10 | P1 | [request.py:22](../legacy/zhenxun_plugin_farm/utils/request.py#L22) | 源代码有硬编码服务令牌,任意持有源码者可取得该值 | 本轮副本已改环境变量;服务端轮换仍未完成 |
| F11 | P2 | [config.py:90](../legacy/zhenxun_plugin_farm/utils/config.py#L90)、[shop.py:243](../legacy/zhenxun_plugin_farm/core/shop.py#L243) | 批量出售选择带 name 占位符的文案却未传 name,资产更新后可能格式化失败 | 统一结果模型,先验证模板参数并用例覆盖 |
F01/F02 的例子来自源码算式,不是完整机器人运行结果。
F06/F07 的失败后果是条件性推断,尚未做数据库故障注入。
F09 已做不依赖宿主的模块定义加载,不能据此声称主插件必然启动失败。
F10 不在报告或代码中重复原令牌。
## 设计问题与待验证风险
| 编号 | 类别 | 证据与判断 | 建议验证/整改 |
|---|---|---|---|
| R01 | 并发风险 | [database.py:125](../legacy/zhenxun_plugin_farm/core/database/database.py#L125) 的 BEGIN/COMMIT 无业务事务协调;[player.py:62](../legacy/zhenxun_plugin_farm/core/player/player.py#L62) 从内存读改写 | 同一用户并发购买/收获、不同用户共享连接、任务取消测试 |
| R02 | 分层 | [userSoil.py:412](../legacy/zhenxun_plugin_farm/core/database/userSoil.py#L412) 决定星级;[userSign.py:257](../legacy/zhenxun_plugin_farm/core/database/userSign.py#L257) 绘图 | 规则上移领域/应用,绘图独立 |
| R03 | 生命周期 | [dbService.py:51](../legacy/zhenxun_plugin_farm/core/dbService.py#L51) 并发安排 init/initDB,不检查 bool 便标记 ready | 明确启动顺序和失败条件;数据库不可用时不得就绪 |
| R04 | 缓存 | [playerPool.py:67](../legacy/zhenxun_plugin_farm/core/player/playerPool.py#L67) 的线程锁只保护字典,不保护返回玩家对象的异步操作 | 数据库为权威,先移除余额写缓存,再按性能需要缓存查询 |
| R05 | 可维护性 | CDBService 和 CDBServiceLocator 两套服务入口,工具层反向加载业务对象 | 保留一个启动组装位置,明确依赖,不机械保留全局单例 |
| R06 | 外部资源 | [request.py:65](../legacy/zhenxun_plugin_farm/utils/request.py#L65) 下载直接覆盖,未验证固定版本和内容哈希 | HTTPS、大小/路径校验、暂存验证、原子切换和最后有效版本 |
| R07 | 扩展机制 | [event.py:60](../legacy/zhenxun_plugin_farm/event/event.py#L60) 事件槽吞异常后继续 | 必需状态更新不依赖这种可失败通知;事件限定提交后副作用 |
“农场下阶段”虽未见入口管理员检查,但 nextPhase 有调试开关且当前
默认 False;本轮不将其认定为默认可利用的权限漏洞。
未来调试功能应显式授权且不进入普通用户命令集。
## 复核与测试优先级
先写行为级回归:出售余额、收获经验、首次注册立即使用、
交易失败不改变任一资产、签到不重复发奖、指定与批量出售锁定一致。
再补并发、时间、迁移和三宿主加载测试。
本轮没有修复 F01–F09/F11,也没有宣布审查通过可发布。
迁移副本唯一安全修正为 F10,旧项目仍保持原样。
+41
View File
@@ -0,0 +1,41 @@
# 待确认与补充问题
这些事项会改变用户资产、玩法和部署结果。文档中的建议不是已确认需求;
本轮初始化和保全不依赖它们,正式业务实现前需收敛关键项。
| 决策 | 建议起点 | 为什么要确认 |
|---|---|---|
| 农场归属 | 每个独立部署自有数据;群共享范围明确配置 | 决定注册唯一键、偷菜范围与迁移 |
| 多框架关系 | 先支持分别部署和迁移,不自动在线互通 | 联机会改变存储与服务架构 |
| 账号迁移 | 显式平台命名空间与人工确认映射 | QQ 官方标识与 OneBot ID 不宜自动合并 |
| 存量资产 | 默认保留并记录异常,另议补偿 | 旧逻辑有增发,无法从余额准确逆推 |
| 外部金币 | 真寻保留可选接入;其他框架默认关闭 | 外部钱包无法由本地事务保证原子性 |
| 点券和星级 | 先核对现有用途、成本与升级公式 | 涉及经济平衡与数据兼容 |
| 签到资源 | 首先保证内置/本地配置可用,联网更新可选 | 远程服务停机不应让基本玩法不可用 |
| 资源分发 | 精简基础资源;需要时使用固定版本扩展包 | 旧资源 ZIP 超 AstrBot 16 MB 市场限制 |
| 渲染 | 文本必备,图片可选且失败降级 | 宿主、字体、浏览器依赖和性能不同 |
| 兼容基线 | 保留旧指令清单;Python/宿主版本经矩阵实测确定 | 旧 README 的 3.10+ 不等于三框架共同最低版本 |
| 发布顺序 | 真寻先稳定,之后普通 NoneBot、AstrBot | 利用现有宿主验证闭环,控制变更范围 |
| 发布仓库 | 使用给出的三个现有仓库,接管前比较现有代码 | 三仓库不为空,不能直接覆盖 |
| 主仓库远程 | 用户后续提供独立 farm 地址 | 本轮只建本地 Git,不误用发布仓库 |
| 发布授权 | 首次候选 PR 审查,稳定后再议自动发布 | 本轮未授权远程覆盖、推送或发布 |
| 跨平台 OS | 建议 Windows + Linux;macOS 有需求再验收 | 路径大小写、字体、浏览器和时区需分别测试 |
| 素材许可证 | 核对图片、字体、HTML 和借用工具来源 | 代码 GPLv3 不自动证明所有素材可分发 |
| 数据运维 | 定期备份、保留策略、恢复演练和脱敏诊断 | 游戏资产需要可恢复性和隐私保护 |
## 容易遗漏的技术约束
- 从旧仓库自动更新到新布局时,插件安装器是否删除旧文件;
旧入口残留可能重复加载,必须进行“旧版→新版”真实升级演练。
- 多目标发布可能部分成功,需要按目标重试和状态清单。
- 插件降级不等于 schema 降级;必须定义最低可读写 schema。
- 网络重试、平台重复投递、图片发送失败不能再次发奖或扣款。
- 资源目录只读部署场景必须支持,把可写缓存放宿主数据目录。
- 同一宿主同时安装相关插件时,模块名称和第三方依赖可能冲突;
插件私有打包只解决前者。
- Python、宿主框架、数据库 schema、作物规则和资源版本分别记录,
不混用一个远程浮点版本比较所有兼容关系。
- 发布仓库里的现有 star、issue、tag 和许可证要保留;不能 force push。
- 子仓库是概念上的发布仓库,不使用 git submodule,也不把本地克隆
提交进主仓库。
- CI 中的密钥不写源码;旧源码中暴露的服务令牌需服务端轮换。
+48
View File
@@ -0,0 +1,48 @@
# 本轮验证与交付边界
日期:2026-10-15。执行 Python:3.12.14;未安装或启动三种宿主。
新工具还以 Python 3.10 语法规则解析旧代码;这不代表三框架共同支持
Python 3.10,最低运行版本需在后续兼容矩阵中确认。
## 已完成
| 检查 | 结果 | 证据 |
|---|---|---|
| Git 初始化 | main 分支,本地仓库,无远程、无提交或推送 | Git 状态;repository-checks.json |
| 迁移完整性 | 827 文件均匹配复制 SHA-256 | scripts/check_baseline.py |
| 来源保持 | 827 文件的旧项目来源哈希未变化 | repository-checks.json |
| Python 语法 | 35 个基线文件通过 | baseline-checks.json、校验脚本 |
| JSON 语法 | 4 个基线配置/版本文件通过 | 校验脚本 |
| 静态数据库 | 两库 quick_check=ok,仅 plant/sqlite_sequence 表 | 校验脚本 |
| 未完成活动模块 | 隔离加载确认两个 NameError;作为已知缺陷记录 | baseline-checks.json、F09 |
| 忽略规则 | 29 类路径均符合预期;827 基线文件未误屏蔽 | repository-checks.json |
| 新工具语法/行长 | 通过;无超 80 字符行 | repository-checks.json |
| 资源 ZIP 测量 | 26,084,694 字节;仅体积探测,未生成安装包 | baseline-checks.json |
| 本地克隆删除 | 三个副本和空父目录均已删除,无子模块 | repository-checks.json |
| 令牌处理 | 迁移副本从环境变量读取;未复制原令牌 | legacy-manifest.json 的唯一差异 |
| 换行保护 | legacy/** -text,避免 Git 检出改变基线字节 | .gitattributes |
本地文档链接复核通过,无缺失目标。校验工具的错误路径也已验证:
模拟 827 个哈希失配均被检测;越界的清单目的路径被拒绝。源码图覆盖存在 metadata_changed 提示,
结论均以直接读取的源码为依据,不把代码图的缺边当作无调用证据。
## 明确未完成
- 旧业务缺陷修复、新核心、多框架插件入口和实际宿主加载。
- 真实数据库事务故障注入、并发压测和全部玩法回归。
- 真实玩家数据库迁移、账号绑定、跨框架数据共享与外部金币对账。
- 跨仓库自动发布、Actions 权限配置、PyPI、Release、商店审核。
- 主仓库远程设置、首次提交、推送或覆盖任一发布仓库。
- 原服务端令牌轮换,素材和字体授权清单、资源瘦身。
## 后续复现
```text
python scripts/check_baseline.py
git status --short
git check-ignore -v runtime/farm.db
git check-ignore -v .release-workspaces/example/.git/config
```
两条 check-ignore 用于观察忽略规则;路径无需真实存在。
脚本不导入插件、不发网络请求、不读玩家数据库。
+31
View File
@@ -0,0 +1,31 @@
# 文档索引
记录日期:2026-10-15。状态:已形成设计草案,待关键产品决策确认;
业务重构、多框架接入、发布流水线和真实数据迁移尚未实施。
| 文档 | 用途 |
|---|---|
| [01-roadmap.md](01-roadmap.md) | 长期目标、实施顺序、里程碑与验收 |
| [02-architecture.md](02-architecture.md) | 分层、目录、依赖、接入与接口设计 |
| [03-business-rules.md](03-business-rules.md) | 身份、货币、库存、作物、事务与幂等 |
| [04-release-design.md](04-release-design.md) | 一个主仓库、三个发布仓库、分包与同步 |
| [05-migration.md](05-migration.md) | 旧代码复用、数据升级、兼容与回滚 |
| [06-code-review.md](06-code-review.md) | 源码证据、缺陷等级、验证状态与修复方向 |
| [07-decisions.md](07-decisions.md) | 需要补充的产品和运维决定 |
| [08-validation.md](08-validation.md) | 本轮实际验证及尚未验证的范围 |
| [legacy-manifest.json](legacy-manifest.json) | 827 个复制文件的来源与 SHA-256 |
| [repository-checks.json](repository-checks.json) | 忽略规则、文档链接、工具错误路径与清理结果 |
| [baseline-checks.json](baseline-checks.json) | 语法、隔离加载、ZIP 体积探测结果 |
| [repository-inventory.json](repository-inventory.json) | 曾克隆仓库的历史元数据;本地副本已按用户要求删除 |
| [task_plan.md](task_plan.md) | 本轮任务及完成状态 |
| [findings.md](findings.md) | 调查发现 |
| [progress.md](progress.md) | 执行记录 |
## 阅读与使用约定
- “已确认”必须有源码、命令或文档来源;“建议”不能当作已接受需求。
- 文档中的目标结构是设计,不表示对应代码已实现。
- 发布仓库是分发目标,不是 Git 子模块;各框架使用相互独立的插件包。
- 主仓库远程地址尚未提供;不将任一插件发布仓库误设为主仓库 origin。
- 用户已撤回拉取要求,且要求删除本地克隆;三个本地检出已删除,不再访问远程。
- 本轮仅修正迁移副本中的硬编码凭据;业务缺陷记录在审查报告,没有隐式修复。
+20
View File
@@ -0,0 +1,20 @@
{
"python": "3.12.14",
"python_syntax_files": 35,
"syntax_errors": [],
"isolated_module_probes": [
{
"path": "core/activity/activity.py",
"result": "NameError",
"detail": "name 'ActivityType' is not defined"
},
{
"path": "core/activity/effect.py",
"result": "NameError",
"detail": "name 'Player' is not defined"
}
],
"baseline_zip_bytes": 26084694,
"zip_is_installable_plugin": false,
"note": "Size probe only. No framework, scheduler, network or player database started."
}
+27
View File
@@ -0,0 +1,27 @@
# 调查发现
## 已确认事实
- 新主目录 D:/Work/farm 起初为空,现已本地初始化为 main,无远程。
- 源旧项目 Beta 提交 b015c6c1997f146f651b677382980d338a306bb2。
- 源业务文件本轮未修改;复制 827 文件,35 个 Python 文件。
- 两个作物数据库只有 plant 和 sqlite_sequence 表,quick_check 正常。
- 基线 ZIP 约 24.9 MiB,超过 AstrBot 发布文档所述市场包 16 MB 上限。
- 两个活动模块虽能语法解析,但隔离加载分别发生未定义类型 NameError。
- 硬编码服务令牌未复制进新仓库,副本改读 FARM_SERVICE_TOKEN。
- 出售、经验、失败返回、注册缓存等确定性问题见审查 F01–F11。
- 三个用户给出的仓库都已有 main 历史,与本地 Beta 布局不同;
已按用户最新要求删除本地克隆,只保留历史元数据和地址。
## 设计建议
- 主仓库唯一开发源,每个框架发布包携带私有、同版本共享核心。
- 领域/应用/存储/展示/接入职责清晰,首期不建立复杂微服务。
- 数据库为业务权威,应用用例拥有事务,操作具备幂等标识和流水。
- 首期独立部署,跨框架在线共享数据是单独需求。
- 发布使用白名单和可审查候选,不直接清空三个已有发布仓库。
## 待确认
身份和群共享范围、存量资产处理、外部经济、资源分发、最低宿主版本、
主仓库远程地址及发布授权,详见 07-decisions.md。
+26
View File
@@ -0,0 +1,26 @@
{
"status": "passed",
"files": 827,
"python_syntax_files": 35,
"json_files": 4,
"catalogues": [
{
"name": "plant.db",
"quick_check": "ok",
"tables": [
"plant",
"sqlite_sequence"
]
},
{
"name": "plant-test.db",
"quick_check": "ok",
"tables": [
"plant",
"sqlite_sequence"
]
}
],
"errors": [],
"scope": "integrity and syntax only; no host or gameplay verified"
}
File diff suppressed because it is too large Load Diff
+20
View File
@@ -0,0 +1,20 @@
# 执行记录
## 2026-10-15
1. 定位空 farm 目录,核对旧项目分支与源提交。
2. 选择 docs 作为唯一计划位置,初始化 Git main。
3. 读取规划与审查要求,代码图查询后对关键问题核对源码。
4. 复制 827 个受版本管理文件与许可证,保留旧项目。
5. 复制前移除硬编码令牌,记录唯一源码差异与逐文件 SHA-256。
6. 测量压缩体积,执行语法、静态数据库与隔离模块检查。
7. 根据用户补充记录三个发布仓库地址并完成 clone;
收到停止后不再远程操作,收到删除要求后验证精确路径并删除三个副本。
8. 完成架构、业务、发布、迁移、审查、决策与验证文档。
9. 建立 Python/农场忽略规则、跨平台换行策略、职责目录和离线校验工具。
10. 离线校验通过:827 哈希、35 Python、4 JSON、两静态库。
11. 29 类忽略规则符合预期;所有基线文件可被跟踪;源哈希未变化。
12. 新工具无超长行;发布检出目录已不存在,无 .gitmodules。
13. 文档链接、差异和工具错误路径在最后交付核对中记录到验证结果。
未实现新业务、未提交或推送、未配置远程发布权限。
+19
View File
@@ -0,0 +1,19 @@
{
"git_branch": "main",
"git_remote": "",
"ignore_cases": 29,
"ignore_errors": [],
"ignored_baseline_files": [],
"broken_local_links": [],
"new_python_overlong_lines": [],
"source_hash_drift": [],
"local_release_workspace_exists": false,
"gitmodules_exists": false,
"helper_negative_checks": {
"hash_mismatch_detected": 827,
"destination_traversal_rejected": true
},
"legacy_text_attribute": "legacy/zhenxun_plugin_farm/command.py: text: unset",
"untracked_candidate_files": 858,
"embedded_git_directories": []
}
+93
View File
@@ -0,0 +1,93 @@
{
"checked_on": "2026-10-15",
"repositories": [
{
"name": "zhenxun_plugin_farm",
"url": "https://github.com/Shu-Ying/zhenxun_plugin_farm.git",
"local_path": ".release-workspaces/zhenxun_plugin_farm",
"branch": "main",
"commit": "ac495ad6e94f414f5a642a0429f51e7affad3b2a",
"remote_head": "origin/main",
"status": "",
"tracked_files": 720,
"root_entries": [
".gitignore",
"LICENSE",
"README.md",
"__init__.py",
"command.py",
"config",
"config.py",
"database",
"dbService.py",
"event",
"farm",
"json.py",
"log",
"request.py",
"resource",
"tool.py"
],
"nested_agent_rules": [],
"local_exists": false,
"observation": "historical metadata collected before user requested deletion"
},
{
"name": "astrbot_plugin_farm",
"url": "https://github.com/Shu-Ying/astrbot_plugin_farm.git",
"local_path": ".release-workspaces/astrbot_plugin_farm",
"branch": "main",
"commit": "1f47aa84611e23a8f0814b9a9d236386a7bf4e13",
"remote_head": "origin/main",
"status": "",
"tracked_files": 727,
"root_entries": [
".gitignore",
"LICENSE",
"README.md",
"_conf_schema.json",
"cfg.py",
"config",
"database",
"dbService.py",
"event",
"farm",
"json.py",
"log",
"main.py",
"metadata.yaml",
"request.py",
"requirements.txt",
"resource",
"tool.py",
"zhenxun_utils"
],
"nested_agent_rules": [],
"local_exists": false,
"observation": "historical metadata collected before user requested deletion"
},
{
"name": "nonebot_plugin_farm",
"url": "https://github.com/Shu-Ying/nonebot_plugin_farm.git",
"local_path": ".release-workspaces/nonebot_plugin_farm",
"branch": "main",
"commit": "4fe60a6bde22375a7f81d2ed60aebfdb4f5fe93f",
"remote_head": "origin/main",
"status": "",
"tracked_files": 720,
"root_entries": [
".github",
".gitignore",
"LICENSE",
"README.md",
"nonebot_plugin_farm",
"pyproject.toml"
],
"nested_agent_rules": [],
"local_exists": false,
"observation": "historical metadata collected before user requested deletion"
}
],
"current_local_state": "deleted-at-user-request",
"network_actions_allowed": false
}
+47
View File
@@ -0,0 +1,47 @@
# Farm 主仓库初始化计划
日期:2026-10-15。负责人:当前主 Agent。
## 目标与状态
本轮初始化与设计交付已完成。业务重构尚未开始,正式产品决策仍待确认。
建立一个开发主仓库、多框架发布仓库的长期方案;保留旧插件源码与资源;
完成 Git、忽略规则、设计和源码审查文档。
## 完成阶段
- [x] 定位 D:/Work/farm;原为空目录。
- [x] 初始化本地 Git,main 分支;未设远程,未提交或推送。
- [x] 复制 827 个旧文件,生成来源清单;副本移除硬编码凭据。
- [x] 完成架构、目录、业务逻辑、发布与迁移规划。
- [x] 完成源码审查、待确认项与风险说明。
- [x] 验证哈希、Python/JSON 语法、静态库和忽略规则。
- [x] 按补充要求记录三个发布仓库地址。
- [x] 用户撤回拉取并要求删除后,删除三个已克隆副本,停止远程访问。
- [x] 本地文档索引与校验结果交付。
## 不属于本轮的工作
真实多框架业务实现、自动发布、远程提交、发布仓库接管、真实用户数据
迁移和服务端令牌轮换,分别按路线和授权推进,不计为本轮已完成实现。
## 约束
- 旧项目 D:/Work/zhenxun_plugin_farm 保持不变。
- 基线:Beta,b015c6c1997f146f651b677382980d338a306bb2。
- 真实玩家数据、凭据、运行时缓存和 Git 元数据不复制进主源码。
- 文档区分事实、设计建议、待确认项与已完成修改。
- 三个发布仓库仅记录地址,不再 clone/fetch/pull/push。
## 执行异常与处理
| 情况 | 处理 |
|---|---|
| 默认命令环境曾有 MXC 启动错误 | 使用针对用户授权目录的提升执行 |
| 规划解析脚本被签名策略拒绝 | 不改系统策略,显式固定 docs 规划位置 |
| 空目录尚非 Git 仓库,diff 失败 | 初始化后验证 |
| 文档首次检查时 08-validation.md 尚未创建 | 创建后重新检查链接 |
| 旧活动模块 NameError | 记录为已确认缺陷,不混为本轮失败 |
| 用户要求停止拉取时三个 clone 已完成 | 如实说明,收到删除要求后逐个校验路径并删除 |
没有尝试绕过用户停止拉取要求,也没有执行远程发布。
+7
View File
@@ -0,0 +1,7 @@
# AstrBot 接入预留位置
发布目标:https://github.com/Shu-Ying/astrbot_plugin_farm.git
负责 AstrBot 事件、消息、配置和生命周期,不导入 NoneBot 或真寻。
未来生成 main.py、metadata.yaml、配置描述、依赖和私有核心。
本目录尚不可安装;必须验证资源体积及实际安装。
远程仓库本轮不再拉取或推送。
+6
View File
@@ -0,0 +1,6 @@
# 普通 NoneBot 接入预留位置
发布目标:https://github.com/Shu-Ying/nonebot_plugin_farm.git
负责 NoneBot 命令、会话、消息、配置与启停,不导入真寻。
发布仓库保留 Python 包构建布局;PyPI 发布流程待实施。
本目录尚不可安装。远程仓库本轮不再拉取或推送。
+6
View File
@@ -0,0 +1,6 @@
# 真寻接入预留位置
发布目标:https://github.com/Shu-Ying/zhenxun_plugin_farm.git
负责命令、配置、真寻金币、数据目录、权限和生命周期。
共享核心通过插件私有路径加载。旧真寻入口仍在 legacy 中,
本目录尚不可安装。远程仓库本轮不再拉取或推送。
+674
View File
@@ -0,0 +1,674 @@
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:
<program> Copyright (C) <year> <name of author>
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>.
+134
View File
@@ -0,0 +1,134 @@
<div align="center">
# 真寻农场(zhenxun_plugin_farm)
<p align="center">
<a href="./LICENSE">
<img src="https://img.shields.io/badge/license-GPL3.0-FE7D37" alt="license">
</a>
<a href="https://www.python.org">
<img src="https://img.shields.io/badge/Python-3.10%2B-blue" alt="Python">
</a>
<a href="https://github.com/astral-sh/ruff">
<img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="ruff">
</a>
<a href="https://nonebot.dev/">
<img src="https://img.shields.io/badge/Nonebot-2.0.0%2B-black" alt="Nonebot">
</a>
<a href="https://github.com/zhenxun-org/zhenxun_bot">
<img src="https://img.shields.io/badge/zhenxun-0.2.4%2B-%23ECD9D3" alt="Python">
</a>
</p>
<p align="center">
[![tencent-qq](https://img.shields.io/badge/%E7%BE%A4-%E7%9C%9F%E5%AF%BB%E5%86%9C%E5%9C%BA%E6%B5%8B%E8%AF%95-%23FF99CC
)](https://qm.qq.com/q/7hsOD4rOw2)
</p>
你是说可以种地对吧🤔?
</div>
---
## 目录
- [真寻农场(zhenxun\_plugin\_farm)](#真寻农场zhenxun_plugin_farm)
- [目录](#目录)
- [农场界面](#农场界面)
- [如何安装](#如何安装)
- [使用指令](#使用指令)
- [更新日志(详细):](#更新日志详细)
- [用户方面](#用户方面)
- [代码方面](#代码方面)
- [待办事宜 `Todo` 列表](#待办事宜-todo-列表)
- [关于](#关于)
- [致谢](#致谢)
- [许可证](#许可证)
---
## 农场界面
![农场界面](./resource/1.png)
---
## 如何安装
方法一(推荐):在小真寻后台的插件商店下载即可<br>
方法二:下载源码放在小真寻`plugin`目录下
---
## 使用指令
| 指令 | 描述 | Tip |
| --- | --- | --- |
| @小真寻 开通农场 | 首次开通农场 | |
| 我的农场 | 你的农场 | |
| 农场详述 | 农场详细信息 | |
| 我的农场币 | 查询农场币 | |
| 种子商店 [筛选关键字] [页数] or [页数] | 查看种子商店 | 当第一个参数为非整数时,会默认进入筛选状态。页数不填默认为1 |
| 购买种子 [种子名称] [数量] | 购买种子 | 数量不填默认为1 |
| 我的种子 | 查询仓库种子 | |
| 播种 [种子名称] [数量] | 播种种子 | 数量不填默认将最大可能播种 |
| 收获 | 收获成熟作物 | |
| 铲除 | 铲除荒废作物 | |
| 我的作物 | 你的作物 | |
| 出售作物 [作物名称] [数量] | 从仓库里向系统售卖作物 | 不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售 |
| 偷菜 @美波理 | 偷别人的菜 | 每人每天只能偷5次 |
| 购买农场币 | 将真寻金币兑换成农场币 | 兑换比例默认为1:2 手续费默认20% |
| 更改农场名 [新的农场名] | 改名 | 农场名称无法存储特殊字符 |
| 农场签到 | 签到 | 需要注意,该项会从服务器拉取签到数据 |
| 土地升级 [地块ID] | 将土地升级,带来收益提升 | 如果土地升级时,土地有播种作物,那么将直接成熟 |
---
## 更新日志[(详细)](./log/log.md):
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## 待办事宜 `Todo` 列表
- [x] 完善我的农场图片,例如左上角显示用户数据
- [ ] 完善升级数据、作物数据、作物图片
- [x] 签到功能
- [x] 在线更新作物信息
- [ ] 添加渔场功能
- [ ] 增加活动、交易行功能
- [ ] 增加交易行总行功能
- [ ] 添加其他游戏种子素材
- [ ] 想不到了,想到再说
---
## 关于
本人毫无任何Python经验,也从未正式的、系统的、完整的去学习Python。如有看到写的不对的地方,欢迎指出,也欢迎任何人一起来开发、完善农场。
素材来均源于互联网,侵权请联系我删除
---
## 致谢
最后感谢以下框架/作者的提供的技术支持(排名不分先后):
- [Nonebot2](https://github.com/nonebot/nonebot2) *✨ 跨平台 Python 异步机器人框架 ✨*
- [zhenxun_bot](https://github.com/zhenxun-org/zhenxun_bot) *最爱真寻的一集*
- [HibiKier](https://github.com/HibiKier) *阿咪为什么是神*
- [ATTomato](https://github.com/ATTomatoo) *流泪偷码头.jpg*
## 许可证
`真寻农场(zhenxun_plugin_farm)`将采用的是`GPLv3`许可证进行开源
+155
View File
@@ -0,0 +1,155 @@
from nonebot import get_driver
from nonebot.plugin import PluginMetadata
from nonebot_plugin_apscheduler import scheduler
from zhenxun.configs.utils import Command, PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.message import MessageUtils
from .command import diuse_farm, diuse_register, reclamation
from .core import (
CHelpManager,
CShopManager,
g_pDBlocator,
g_pSqlManager,
g_pUserPool,
getFarmManager,
getSignInManager,
)
from .core.dbService import g_pDBService
from .event import g_pEventManager
from .utils import config, getJsonManager, getRequestManager
__plugin_meta__ = PluginMetadata(
name="真寻农场",
description="快乐的农场时光",
usage="""
你也要种地?
指令:
at 开通农场
我的农场
农场详述
我的农场币
种子商店 [筛选关键字] [页数] or [页数]
购买种子 [作物/种子名称] [数量]
我的种子
播种 [作物/种子名称] [数量] (数量不填默认将最大可能播种
收获
铲除
我的作物
出售作物 [作物/种子名称] [数量] (不填写作物名将售卖仓库种全部作物 填作物名不填数量将指定作物全部出售
偷菜 at (每人每天只能偷5次
开垦
购买农场币 [数量] 数量为消耗金币的数量
更改农场名 [新农场名]
农场签到
土地升级 [地块ID](通过农场详述获取)
""".strip(),
extra=PluginExtraData(
author="Art_Sakura",
version="1.5.2",
commands=[Command(command="我的农场")],
menu_type="群内小游戏",
configs=[
RegisterConfig(
key="绘制农场清晰度",
value="low",
help="我的农场返回图片的清晰度, [low, medium, hight, original]",
default_value="low",
),
RegisterConfig(
key="动态背景",
value=True,
help="是否开启根据季节和节日变化的动态背景",
type=bool,
default_value=True,
),
RegisterConfig(
key="签到图片样式",
value="text",
help="签到图片的样式, [text, html]",
default_value="text",
),
RegisterConfig(
key="兑换倍数",
value="2",
help="金币兑换农场币的倍数 默认值为: 2倍",
default_value="2",
),
RegisterConfig(
key="点券兑换倍数",
value="2000",
help="农场币兑换点券的倍数 比例为2000:1",
default_value="2000",
),
RegisterConfig(
key="手续费",
value="0.2",
help="金币兑换农场币的手续费 默认值为: 0.2 实际意义为20%手续费",
default_value="0.2",
),
RegisterConfig(
key="服务地址",
value="http://diuse.work",
help="签到、交易行、活动等服务器地址",
default_value="http://diuse.work",
),
],
).to_dict(),
)
driver = get_driver()
# 构造函数
@driver.on_startup
async def start():
# 数据库加载
await g_pSqlManager.init()
# 初始化数据库变量 和 加载作物数据库
await g_pDBService.init()
# 检查作物文件是否缺失 or 更新
await getRequestManager().initPlantDBFile()
await CHelpManager.createHelpImage()
# todayRewards = [
# {
# "imagePath": "../plant/ATTomato/icon.png",
# "name": "金币",
# "quantity": 100,
# },
# ]
# logger.info(f"{todayRewards[0]}")
# cumulativeRewards = [
# {
# "imagePath": g_sResourcePath / "/plant/ATTomato/icon.png",
# "name": "金币",
# "quantity": 200,
# "description": "累计签到1次",
# "achieved": True,
# },
# ]
# await g_pSignInManager.createSignImage(todayRewards, cumulativeRewards, 1, 1, 1)
# 析构函数
@driver.on_shutdown
async def shutdown():
# 单独卸载 作物数据库
await g_pDBService.cleanup()
await g_pSqlManager.cleanup()
@scheduler.scheduled_job(trigger="cron", hour=4, minute=30, id="signInFile")
async def signInFile():
try:
await getJsonManager().initSignInFile()
await getRequestManager().initPlantDBFile()
except Exception as e:
logger.error("农场定时检查出错", e=e)
+737
View File
@@ -0,0 +1,737 @@
import inspect
from nonebot.adapters import Event
from nonebot.rule import to_me
from nonebot_plugin_alconna import (
Alconna,
AlconnaQuery,
Args,
At,
Match,
MultiVar,
Option,
Query,
Subcommand,
on_alconna,
store_true,
)
from nonebot_plugin_uninfo import Uninfo
from nonebot_plugin_waiter import waiter
from zhenxun.configs.config import BotConfig
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from zhenxun.utils.message import MessageUtils
from .core import CShopManager, g_pDBlocator, getFarmManager, getSignInManager
from .utils import config, getDrawFarm, getToolManager
diuse_register = on_alconna(
Alconna("开通农场"),
priority=5,
rule=to_me(),
block=True,
use_cmd_start=True,
)
@diuse_register.handle()
async def handle_register(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is not None and await player.isRegistered():
await MessageUtils.build_message(
config.g_sTranslation["register"]["repeat"]
).send(reply_to=True)
return
try:
raw_name = str(session.user.name)
safe_name = getToolManager().sanitize_username(raw_name)
success = await g_pDBlocator.getUserManager().initUserInfo(uid, safe_name)
logger.info(f"用户 {uid} 选择的农场名称为: {raw_name} | 过滤后为: {safe_name}")
msg = (
config.g_sTranslation["register"]["success"].format(point=500)
if success
else config.g_sTranslation["register"]["error"]
)
logger.info(f"用户注册 {'成功' if success else '失败'}:{uid}")
except Exception as e:
msg = config.g_sTranslation["register"]["error"]
logger.error(f"注册异常 | UID:{uid} | 错误:{e}")
await MessageUtils.build_message(msg).send(reply_to=True)
diuse_farm = on_alconna(
Alconna(
"我的农场",
Option("--all", action=store_true),
Subcommand("detail", help_text="农场详述"),
Subcommand("my-point", help_text="我的农场币"),
Subcommand("seed-shop", Args["res?", MultiVar(str)], help_text="种子商店"),
Subcommand("buy-seed", Args["name?", str]["num?", int], help_text="购买种子"),
Subcommand("my-seed", help_text="我的种子"),
Subcommand("sowing", Args["name?", str]["num?", int], help_text="播种"),
Subcommand("harvest", help_text="收获"),
Subcommand("eradicate", help_text="铲除"),
Subcommand("my-plant", help_text="我的作物"),
Subcommand("lock-plant", Args["name?", str], help_text="作物加锁"),
Subcommand("unlock-plant", Args["name?", str], help_text="作物解锁"),
Subcommand("sell-plant", Args["name?", str]["num?", int], help_text="出售作物"),
Subcommand("stealing", Args["target?", At], help_text="偷菜"),
Subcommand("buy-point", Args["num?", int], help_text="购买农场币"),
# Subcommand("sell-point", Args["num?", int], help_text="转换金币")
Subcommand("change-name", Args["name?", str], help_text="更改农场名"),
Subcommand("sign-in", help_text="农场签到"),
Subcommand("admin-up", Args["num?", int], help_text="农场下阶段"),
Subcommand("point-to-vipPoint", Args["num?", int], help_text="点券兑换"),
Subcommand("my-vipPoint", help_text="我的点券"),
Subcommand("farm-help", help_text="农场帮助"),
Subcommand("vipSeed-shop", Args["res?", MultiVar(str)], help_text="种子商店"),
),
priority=5,
block=True,
use_cmd_start=True,
)
@diuse_farm.assign("$main")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
image = await getDrawFarm().drawFarmByUid(uid)
await MessageUtils.build_message(image).send(reply_to=True)
diuse_farm.shortcut(
"农场详述",
command="我的农场",
arguments=["detail"],
prefix=True,
)
@diuse_farm.assign("detail")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
info = await getDrawFarm().drawDetailFarmByUid(uid)
await MessageUtils.alc_forward_msg(
[info], session.self_id, BotConfig.self_nickname
).send()
diuse_farm.shortcut(
"我的农场币",
command="我的农场",
arguments=["my-point"],
prefix=True,
)
@diuse_farm.assign("my-point")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
point = player.user["point"]
if point < 0:
await MessageUtils.build_message(
config.g_sTranslation["basic"]["notFarm"]
).send()
return False
await MessageUtils.build_message(
config.g_sTranslation["basic"]["point"].format(point=point)
).send(reply_to=True)
diuse_farm.shortcut(
"种子商店(.*?)",
command="我的农场",
arguments=["seed-shop"],
prefix=True,
)
@diuse_farm.assign("seed-shop")
async def _(session: Uninfo, res: Match[tuple[str, ...]]):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
if res.result is inspect._empty:
raw = []
else:
raw = res.result
filterKey: str | int | None = None
page: int = 1
if len(raw) >= 1 and raw[0] is not None:
first = raw[0]
if isinstance(first, str) and first.isdigit():
page = int(first)
else:
filterKey = first
if (
len(raw) >= 2
and raw[1] is not None
and isinstance(raw[1], str)
and raw[1].isdigit()
):
page = int(raw[1])
if filterKey is None:
image = await CShopManager.getSeedShopImage(page, 0, 0)
else:
image = await CShopManager.getSeedShopImage(filterKey, page, 0)
await MessageUtils.build_message(image).send()
diuse_farm.shortcut(
"购买种子(?P<name>.*?)",
command="我的农场",
arguments=["buy-seed", "{name}"],
prefix=True,
)
@diuse_farm.assign("buy-seed")
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", 1)
):
if not name.available:
await MessageUtils.build_message(
config.g_sTranslation["buySeed"]["notSeed"]
).finish(reply_to=True)
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await CShopManager.buySeed(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的种子",
command="我的农场",
arguments=["my-seed"],
prefix=True,
)
@diuse_farm.assign("my-seed")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().getUserSeedByUid(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"播种(?P<name>.*?)",
command="我的农场",
arguments=["sowing", "{name}"],
prefix=True,
)
@diuse_farm.assign("sowing")
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)
):
if not name.available:
await MessageUtils.build_message(
config.g_sTranslation["sowing"]["notSeed"]
).finish(reply_to=True)
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().sowing(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"收获",
command="我的农场",
arguments=["harvest"],
prefix=True,
)
@diuse_farm.assign("harvest")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().harvest(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"铲除",
command="我的农场",
arguments=["eradicate"],
prefix=True,
)
@diuse_farm.assign("eradicate")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().eradicate(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的作物",
command="我的农场",
arguments=["my-plant"],
prefix=True,
)
@diuse_farm.assign("my-plant")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().getUserPlantByUid(uid)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"作物加锁(?P<name>)",
command="我的农场",
arguments=["lock-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("lock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().lockUserPlantByUid(uid, name.result, 1)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"作物解锁(?P<name>)",
command="我的农场",
arguments=["unlock-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("unlock-plant")
async def _(session: Uninfo, name: Match[str]):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().lockUserPlantByUid(uid, name.result, 0)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"出售作物(?P<name>.*?)",
command="我的农场",
arguments=["sell-plant", "{name}"],
prefix=True,
)
@diuse_farm.assign("sell-plant")
async def _(
session: Uninfo, name: Match[str], num: Query[int] = AlconnaQuery("num", -1)
):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await CShopManager.sellPlantByUid(uid, name.result, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
reclamation = on_alconna(
Alconna("开垦"),
priority=5,
block=True,
use_cmd_start=True,
)
@reclamation.handle()
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
condition = await getFarmManager().reclamationCondition(uid)
condition += f"\n{config.g_sTranslation['reclamation']['confirm']}"
await MessageUtils.build_message(condition).send(reply_to=True)
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message(
config.g_sTranslation["reclamation"]["timeOut"]
).send(reply_to=True)
return
if not resp == "是":
return
res = await getFarmManager().reclamation(uid)
await MessageUtils.build_message(res).send(reply_to=True)
diuse_farm.shortcut(
"偷菜",
command="我的农场",
arguments=["stealing"],
prefix=True,
)
@diuse_farm.assign("stealing")
async def _(session: Uninfo, target: Match[At]):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
if not target.available:
await MessageUtils.build_message(
config.g_sTranslation["stealing"]["noTarget"]
).finish(reply_to=True)
tar = target.result
result = await g_pDBlocator.getUserManager().isRegistered(tar.target)
if not result:
await MessageUtils.build_message(
config.g_sTranslation["stealing"]["targetNotFarm"]
).send()
return None
result = await getFarmManager().stealing(uid, tar.target)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"购买农场币(.*?)",
command="我的农场",
arguments=["buy-point"],
prefix=True,
)
@diuse_farm.assign("buy-point")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
if num.result <= 0:
await MessageUtils.build_message("请在指令后跟需要购买农场币的数量").finish(
reply_to=True
)
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().buyPointByUid(uid, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"更改农场名(?P<name>)",
command="我的农场",
arguments=["change-name", "{name}"],
prefix=True,
)
@diuse_farm.assign("change-name")
async def _(session: Uninfo, name: Match[str]):
if not name.available:
await MessageUtils.build_message(
config.g_sTranslation["changeName"]["noName"]
).finish(reply_to=True)
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
player = await getToolManager().getPlayerByUid(uid)
if player is None:
await MessageUtils.build_message(
config.g_sTranslation["changeName"]["error1"]
).send(reply_to=True)
return
result = await player.updateName(name.result)
await MessageUtils.build_message(config.g_sTranslation["changeName"][result]).send(
reply_to=True
)
diuse_farm.shortcut(
"农场签到",
command="我的农场",
arguments=["sign-in"],
prefix=True,
)
@diuse_farm.assign("sign-in")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
# 判断签到是否正常加载
if not config.g_bSignStatus:
await MessageUtils.build_message(
config.g_sTranslation["signIn"]["error"]
).send()
return
message = await getSignInManager().signInByUid(uid)
await MessageUtils.build_message(message).send()
soil_upgrade = on_alconna(
Alconna("土地升级", Args["index", int]),
priority=5,
block=True,
use_cmd_start=True,
)
@soil_upgrade.handle()
async def _(session: Uninfo, index: Query[int] = AlconnaQuery("index", 1)):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
condition = await getFarmManager().soilUpgradeCondition(uid, index.result)
await MessageUtils.build_message(condition).send(reply_to=True)
if not condition.startswith("将土地升级至:"):
return
@waiter(waits=["message"], keep_session=True)
async def check(event: Event):
return event.get_plaintext()
resp = await check.wait(timeout=60)
if resp is None:
await MessageUtils.build_message(
config.g_sTranslation["soilInfo"]["timeOut"]
).send(reply_to=True)
return
if not resp == "是":
return
res = await getFarmManager().soilUpgrade(uid, index.result)
await MessageUtils.build_message(res).send(reply_to=True)
diuse_farm.shortcut(
"农场下阶段(.*?)",
command="我的农场",
arguments=["admin-up"],
prefix=True,
)
@diuse_farm.assign("admin-up")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
await g_pDBlocator.getUserSoilManager().nextPhase(uid, num.result)
diuse_farm.shortcut(
"点券兑换(.*?)",
command="我的农场",
arguments=["point-to-vipPoint"],
prefix=True,
)
@diuse_farm.assign("point-to-vipPoint")
async def _(session: Uninfo, num: Query[int] = AlconnaQuery("num", 0)):
if num.result <= 0:
await MessageUtils.build_message("请在指令后跟需要购买点券的数量").finish(
reply_to=True
)
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
result = await getFarmManager().pointToVipPointByUid(uid, num.result)
await MessageUtils.build_message(result).send(reply_to=True)
diuse_farm.shortcut(
"我的点券",
command="我的农场",
arguments=["my-vipPoint"],
prefix=True,
)
@diuse_farm.assign("my-vipPoint")
async def _(session: Uninfo):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
await MessageUtils.build_message(
config.g_sTranslation["basic"]["vipPoint"].format(
vipPoint=player.user["vipPoint"]
)
).send(reply_to=True)
diuse_farm.shortcut(
"农场帮助",
command="我的农场",
arguments=["farm-help"],
prefix=True,
)
@diuse_farm.assign("farm-help")
async def _(session: Uninfo):
savePath = DATA_PATH / "farm_res/html/help.png"
image = BuildImage(background=savePath)
await MessageUtils.build_message(image).send(reply_to=True)
diuse_farm.shortcut(
"点券商店(.*?)",
command="我的农场",
arguments=["vipSeed-shop"],
prefix=True,
)
@diuse_farm.assign("vipSeed-shop")
async def _(session: Uninfo, res: Match[tuple[str, ...]]):
uid = str(session.user.id)
player = await getToolManager().getPlayerByUid(uid)
if player is None or not await player.isRegistered():
await getToolManager().repeat()
return
if res.result is inspect._empty:
raw = []
else:
raw = res.result
filterKey: str | int | None = None
page: int = 1
if len(raw) >= 1 and raw[0] is not None:
first = raw[0]
if isinstance(first, str) and first.isdigit():
page = int(first)
else:
filterKey = first
if (
len(raw) >= 2
and raw[1] is not None
and isinstance(raw[1], str)
and raw[1].isdigit()
):
page = int(raw[1])
if filterKey is None:
image = await CShopManager.getSeedShopImage(page, 0, 1)
else:
image = await CShopManager.getSeedShopImage(filterKey, page, 1)
await MessageUtils.build_message(image).send()
@@ -0,0 +1,22 @@
{
"version": 0.01,
"zhuShi":
[
"name: 名称",
"level: 等级",
"price: 单价",
"icon: 显示图片",
"sell: 是否可以上架交易行"
],
"item":
{
"muBan":
{
"name": "木板",
"level": 0,
"price": 5000,
"icon": "Test",
"sell": true
}
}
}
@@ -0,0 +1,172 @@
{
"version": 0.01,
"soil":[1, 1, 1, 2, 3, 5, 7, 9, 11, 13,
15, 17, 19, 21, 23, 25, 27, 29, 31, 33,
36, 39, 42, 45, 48, 51, 54, 57, 60, 70
],
"reclamation":
{
"4":
{
"level": 2,
"point": 800,
"item": []
},
"5":
{
"level": 3,
"point": 1300,
"item": []
},
"6":
{
"level": 5,
"point": 3200,
"item": []
},
"7":
{
"level": 7,
"point": 5500,
"item": []
},
"8":
{
"level": 9,
"point": 12000,
"item": []
},
"9":
{
"level": 11,
"point": 19800,
"item": []
},
"10":
{
"level": 13,
"point": 30000,
"item": []
},
"11":
{
"level": 15,
"point": 50000,
"item": []
},
"12":
{
"level": 17,
"point": 70000,
"item": []
},
"13":
{
"level": 19,
"point": 90000,
"item": []
},
"14":
{
"level": 21,
"point": 130000,
"item": []
},
"15":
{
"level": 23,
"point": 150000,
"item": []
},
"16":
{
"level": 25,
"point": 180000,
"item": []
},
"17":
{
"level": 27,
"point": 230000,
"item": []
},
"18":
{
"level": 29,
"point": 300000,
"item": []
},
"19":
{
"level": 31,
"point": 500000,
"item": []
},
"20":
{
"level": 33,
"point": 850000,
"item": []
},
"21":
{
"level": 36,
"point": 1100000,
"item": []
},
"22":
{
"level": 39,
"point": 1300000,
"item": []
},
"23":
{
"level": 42,
"point": 1500000,
"item": []
},
"24":
{
"level": 45,
"point": 1700000,
"item": []
},
"25":
{
"level": 48,
"point": 2000000,
"item": []
},
"26":
{
"level": 51,
"point": 2200000,
"item": []
},
"27":
{
"level": 54,
"point": 2800000,
"item": []
},
"28":
{
"level": 57,
"point": 3500000,
"item": []
},
"29":
{
"level": 60,
"point": 4300000,
"item": []
},
"30":
{
"level": 70,
"point": 5200000,
"item": []
}
}
}
+256
View File
@@ -0,0 +1,256 @@
{
"version": 0.01,
"size":[234, 156],
"soil":
{
"1":
{
"x": 1352,
"y": 552
},
"2":
{
"x": 1462,
"y": 611
},
"3":
{
"x": 1572,
"y": 670
},
"4":
{
"x": 1682,
"y": 729
},
"5":
{
"x": 1792,
"y": 788
},
"6":
{
"x": 1902,
"y": 847
},
"7":
{
"x": 1237,
"y": 606
},
"8":
{
"x": 1350,
"y": 665
},
"9":
{
"x": 1457,
"y": 724
},
"10":
{
"x": 1567,
"y": 783
},
"11":
{
"x": 1677,
"y": 842
},
"12":
{
"x": 1787,
"y": 901
},
"13":
{
"x": 1116,
"y": 663
},
"14":
{
"x": 1226,
"y": 722
},
"15":
{
"x": 1336,
"y": 782
},
"16":
{
"x": 1455,
"y": 840
},
"17":
{
"x": 1565,
"y": 899
},
"18":
{
"x": 1671,
"y": 958
},
"19":
{
"x": 1002,
"y": 720
},
"20":
{
"x": 1105,
"y": 779
},
"21":
{
"x": 1224,
"y": 838
},
"22":
{
"x": 1343,
"y": 897
},
"23":
{
"x": 1449,
"y": 956
},
"24":
{
"x": 1563,
"y": 1015
},
"25":
{
"x": 883,
"y": 777
},
"26":
{
"x": 995,
"y": 836
},
"27":
{
"x": 1112,
"y": 895
},
"28":
{
"x": 1222,
"y": 954
},
"29":
{
"x": 1336,
"y": 1013
},
"30":
{
"x": 1451,
"y": 1072
}
},
"upgrade":
{
"red": [
{ "level": 28, "point": 200000, "vipPoint": 0, "item":{}},
{ "level": 29, "point": 220000, "vipPoint": 0, "item":{}},
{ "level": 30, "point": 240000, "vipPoint": 0, "item":{}},
{ "level": 31, "point": 260000, "vipPoint": 0, "item":{}},
{ "level": 32, "point": 290000, "vipPoint": 0, "item":{}},
{ "level": 33, "point": 320000, "vipPoint": 0, "item":{}},
{ "level": 34, "point": 350000, "vipPoint": 0, "item":{}},
{ "level": 35, "point": 380000, "vipPoint": 0, "item":{}},
{ "level": 36, "point": 410000, "vipPoint": 0, "item":{}},
{ "level": 37, "point": 440000, "vipPoint": 0, "item":{}},
{ "level": 38, "point": 480000, "vipPoint": 0, "item":{}},
{ "level": 39, "point": 520000, "vipPoint": 0, "item":{}},
{ "level": 40, "point": 560000, "vipPoint": 0, "item":{}},
{ "level": 41, "point": 600000, "vipPoint": 0, "item":{}},
{ "level": 42, "point": 650000, "vipPoint": 0, "item":{}},
{ "level": 43, "point": 700000, "vipPoint": 0, "item":{}},
{ "level": 44, "point": 770000, "vipPoint": 0, "item":{}},
{ "level": 45, "point": 900000, "vipPoint": 0, "item":{}},
{ "level": 47, "point": 1500000, "vipPoint": 0, "item":{}},
{ "level": 49, "point": 2000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 55, "point": 5500000, "vipPoint": 0, "item":{}},
{ "level": 57, "point": 6800000, "vipPoint": 0, "item":{}},
{ "level": 60, "point": 10000000, "vipPoint": 0, "item":{}},
{ "level": 64, "point": 15000000, "vipPoint": 0, "item":{}},
{ "level": 68, "point": 20000000, "vipPoint": 0, "item":{}},
{ "level": 73, "point": 30000000, "vipPoint": 0, "item":{}},
{ "level": 78, "point": 50000000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 80000000, "vipPoint": 0, "item":{}}
],
"black": [
{ "level": 40, "point": 500000, "vipPoint": 0, "item":{}},
{ "level": 41, "point": 600000, "vipPoint": 0, "item":{}},
{ "level": 42, "point": 700000, "vipPoint": 0, "item":{}},
{ "level": 43, "point": 800000, "vipPoint": 0, "item":{}},
{ "level": 44, "point": 900000, "vipPoint": 0, "item":{}},
{ "level": 45, "point": 1000000, "vipPoint": 0, "item":{}},
{ "level": 46, "point": 2000000, "vipPoint": 0, "item":{}},
{ "level": 47, "point": 2200000, "vipPoint": 0, "item":{}},
{ "level": 48, "point": 2400000, "vipPoint": 0, "item":{}},
{ "level": 49, "point": 2600000, "vipPoint": 0, "item":{}},
{ "level": 50, "point": 2800000, "vipPoint": 0, "item":{}},
{ "level": 51, "point": 3000000, "vipPoint": 0, "item":{}},
{ "level": 52, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 53, "point": 4200000, "vipPoint": 0, "item":{}},
{ "level": 54, "point": 4400000, "vipPoint": 0, "item":{}},
{ "level": 55, "point": 4600000, "vipPoint": 0, "item":{}},
{ "level": 56, "point": 4800000, "vipPoint": 0, "item":{}},
{ "level": 57, "point": 5000000, "vipPoint": 0, "item":{}},
{ "level": 59, "point": 6000000, "vipPoint": 0, "item":{}},
{ "level": 61, "point": 6200000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 67, "point": 6800000, "vipPoint": 0, "item":{}},
{ "level": 69, "point": 7000000, "vipPoint": 0, "item":{}},
{ "level": 72, "point": 10000000, "vipPoint": 0, "item":{}},
{ "level": 76, "point": 16000000, "vipPoint": 0, "item":{}},
{ "level": 80, "point": 27000000, "vipPoint": 0, "item":{}},
{ "level": 85, "point": 43000000, "vipPoint": 0, "item":{}},
{ "level": 90, "point": 65000000, "vipPoint": 0, "item":{}},
{ "level": 95, "point": 93000000, "vipPoint": 0, "item":{}}
],
"gold": [
{ "level": 58, "point": 4000000, "vipPoint": 0, "item":{}},
{ "level": 59, "point": 4200000, "vipPoint": 0, "item":{}},
{ "level": 60, "point": 4400000, "vipPoint": 0, "item":{}},
{ "level": 61, "point": 4600000, "vipPoint": 0, "item":{}},
{ "level": 62, "point": 4800000, "vipPoint": 0, "item":{}},
{ "level": 63, "point": 5000000, "vipPoint": 0, "item":{}},
{ "level": 64, "point": 5200000, "vipPoint": 0, "item":{}},
{ "level": 65, "point": 5400000, "vipPoint": 0, "item":{}},
{ "level": 66, "point": 5600000, "vipPoint": 0, "item":{}},
{ "level": 67, "point": 5800000, "vipPoint": 0, "item":{}},
{ "level": 68, "point": 6000000, "vipPoint": 0, "item":{}},
{ "level": 69, "point": 6200000, "vipPoint": 0, "item":{}},
{ "level": 70, "point": 6600000, "vipPoint": 0, "item":{}},
{ "level": 71, "point": 7000000, "vipPoint": 0, "item":{}},
{ "level": 72, "point": 7400000, "vipPoint": 0, "item":{}},
{ "level": 73, "point": 7800000, "vipPoint": 0, "item":{}},
{ "level": 74, "point": 8200000, "vipPoint": 0, "item":{}},
{ "level": 75, "point": 8600000, "vipPoint": 0, "item":{}},
{ "level": 77, "point": 9500000, "vipPoint": 0, "item":{}},
{ "level": 79, "point": 10400000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 12200000, "vipPoint": 0, "item":{}},
{ "level": 83, "point": 12200000, "vipPoint": 0, "item":{}},
{ "level": 85, "point": 13100000, "vipPoint": 0, "item":{}},
{ "level": 87, "point": 14000000, "vipPoint": 0, "item":{}},
{ "level": 90, "point": 16000000, "vipPoint": 0, "item":{}},
{ "level": 94, "point": 20000000, "vipPoint": 0, "item":{}},
{ "level": 98, "point": 28000000, "vipPoint": 0, "item":{}},
{ "level": 103, "point": 44000000, "vipPoint": 0, "item":{}},
{ "level": 108, "point": 76000000, "vipPoint": 0, "item":{}},
{ "level": 113, "point": 1080000000, "vipPoint": 0, "item":{}}
]
}
}
@@ -0,0 +1,29 @@
from .database import g_pDBlocator, g_pSqlManager
from .help import CHelpManager
from .player.player import CPlayer
from .player.playerPool import g_pUserPool
from .shop import CShopManager
def getFarmManager():
from .farm import CFarmManager
return CFarmManager()
def getSignInManager():
from .activity.sign_in import CSignInManager
return CSignInManager()
__all__ = [
"CHelpManager",
"CPlayer",
"CShopManager",
"g_pDBlocator",
"g_pSqlManager",
"g_pUserPool",
"getFarmManager",
"getSignInManager",
]
@@ -0,0 +1,108 @@
class ActivityManager:
"""活动管理器"""
def __init__(self):
self.activities = {} # 活动ID -> 活动配置
self.active_activities = set() # 当前活跃的活动ID
self.effect_handlers = {} # 效果类型 -> 处理器
# 注册效果处理器
self._register_handlers()
def _register_handlers(self):
"""注册所有效果处理器"""
self.effect_handlers[EffectType.MULTIPLIER] = MultiplierHandler()
self.effect_handlers[EffectType.FIXED_BONUS] = FixedBonusHandler()
self.effect_handlers[EffectType.BUFF_APPLICATION] = BuffApplicationHandler()
self.effect_handlers[EffectType.QUEST_TRIGGER] = QuestTriggerHandler()
def load_activities_from_config(self, config_path: str):
"""从配置文件加载活动"""
with open(config_path, encoding="utf-8") as f:
config = json.load(f)
for activity_data in config["activities"]:
activity = Activity(activity_data)
self.activities[activity.id] = activity
print(f"加载活动: {activity.name}")
def update_activity_status(self):
"""更新活动状态(定时调用)"""
now = datetime.now()
for activity in self.activities.values():
is_active = activity.start_time <= now <= activity.end_time
if is_active and activity.id not in self.active_activities:
# 活动开始
self.active_activities.add(activity.id)
print(f"活动开始: {activity.name}")
elif not is_active and activity.id in self.active_activities:
# 活动结束
self.active_activities.remove(activity.id)
print(f"活动结束: {activity.name}")
def get_active_activities(self, activity_type: ActivityType = None) -> List:
"""获取当前活跃的活动"""
active_list = []
for activity_id in self.active_activities:
activity = self.activities[activity_id]
if activity_type is None or activity.activity_type == activity_type:
active_list.append(activity)
return active_list
def apply_activity_effects(
self,
activity_type: ActivityType,
player: Player,
base_value: int,
context: Dict = None,
) -> int:
"""应用活动效果"""
if context is None:
context = {}
context["base_value"] = base_value
result = base_value
# 获取同类型的所有活跃活动
active_activities = self.get_active_activities(activity_type)
for activity in active_activities:
print(f"为玩家 {player.player_id} 应用活动: {activity.name}")
for effect in activity.effects:
handler = self.effect_handlers.get(effect.type)
if handler:
try:
effect_result = handler.execute(effect.params, player, context)
if effect_result is not None:
result = effect_result
context["base_value"] = result # 更新基础值供后续效果使用
except Exception as e:
print(f"效果执行失败: {effect.type}, 错误: {e}")
return result
class Activity:
"""活动类"""
def __init__(self, data: Dict):
self.id = data["id"]
self.name = data["name"]
self.activity_type = ActivityType(data["activity_type"])
self.start_time = datetime.strptime(data["start_time"], "%Y-%m-%d %H:%M:%S")
self.end_time = datetime.strptime(data["end_time"], "%Y-%m-%d %H:%M:%S")
self.effects = [Effect(effect_data) for effect_data in data["effects"]]
class Effect:
"""效果类"""
def __init__(self, data: Dict):
self.type = EffectType(data["type"])
self.params = data.get("params", {})
@@ -0,0 +1,95 @@
from enum import Enum
from typing import Any
# 活动类型枚举
class ActivityType(Enum):
PLANTING = "planting" # 种植活动
HARVESTING = "harvesting" # 收获活动
FISHING = "fishing" # 钓鱼活动
COMBAT = "combat" # 战斗活动
# 效果类型枚举
class EffectType(Enum):
MULTIPLIER = "multiplier" # 倍数加成
FIXED_BONUS = "fixed_bonus" # 固定加成
BUFF_APPLICATION = "buff_application" # 施加BUFF
QUEST_TRIGGER = "quest_trigger" # 任务触发
class EffectHandler:
"""
效果处理器基类
"""
def execute(self, params: dict, uid: str, context: dict) -> Any:
raise NotImplementedError
class MultiplierHandler(EffectHandler):
"""
倍数效果处理器
"""
def execute(self, params: dict, uid: str, context: dict) -> float:
base_value = context.get("base_value", 0)
multiplier = params.get("value", 1.0)
# 检查条件
if self._check_conditions(params.get("conditions", {}), player):
result = base_value * multiplier
print(f"倍数效果: {base_value} × {multiplier} = {result}")
return result
return base_value
def _check_conditions(self, conditions: dict, player: Player) -> bool:
"""检查生效条件"""
# 等级要求
min_level = conditions.get("min_level", 0)
if player.level < min_level:
return False
# 需要特定物品
required_items = conditions.get("required_items", [])
for item in required_items:
if player.inventory.get(item, 0) <= 0:
return False
return True
class FixedBonusHandler(EffectHandler):
"""固定加成处理器"""
def execute(self, params: dict, player: Player, context: dict) -> int:
base_value = context.get("base_value", 0)
bonus = params.get("value", 0)
result = base_value + bonus
print(f"固定加成: {base_value} + {bonus} = {result}")
return result
class BuffApplicationHandler(EffectHandler):
"""BUFF应用处理器"""
def execute(self, params: dict, player: Player, context: dict) -> None:
buff_id = params["buff_id"]
duration = params.get("duration", 3600) # 默认1小时
properties = params.get("properties", {})
player.add_buff(buff_id, duration, properties)
# 立即应用BUFF效果(如果有)
if "immediate_effect" in params:
self._apply_immediate_effect(params["immediate_effect"], player)
class QuestTriggerHandler(EffectHandler):
"""任务触发处理器"""
def execute(self, params: dict, player: Player, context: dict) -> None:
quest_id = params["quest_id"]
print(f"为玩家 {player.player_id} 触发任务: {quest_id}")
# 这里会调用任务系统来分配任务
@@ -0,0 +1,152 @@
from datetime import datetime
from typing import Any
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from ...utils import config, getJsonManager, getToolManager
from ..dbService import g_pDBService
class CSignInManager:
@classmethod
async def signInByUid(cls, uid: str) -> str:
toDay = getToolManager().dateTime().date().today()
message = ""
status = await g_pDBService.userSign.sign(uid, toDay.strftime("%Y-%m-%d"))
# 如果完成签到
if status == 1 or status == 2:
# 获取签到总天数
signDay = await g_pDBService.userSign.getUserSignCountByDate(
uid, toDay.strftime("%Y-%m")
)
exp, point = await g_pDBService.userSign.getUserSignRewardByDate(
uid, toDay.strftime("%Y-%m-%d")
)
message += config.g_sTranslation["signIn"]["success"].format(
day=signDay, exp=exp, num=point
)
jSign = await getJsonManager().getSign()
reward = jSign["continuou"].get(f"{signDay}", None)
signType = Config.get_config("zhenxun_plugin_farm", "签到图片样式")
if reward:
extraPoint = reward.get("point", 0)
extraExp = reward.get("exp", 0)
plant = reward.get("plant", {})
message += config.g_sTranslation["signIn"]["grandTotal"].format(
exp=extraExp, num=extraPoint
)
vipPoint = reward.get("vipPoint", 0)
if vipPoint > 0:
message += config.g_sTranslation["signIn"]["grandTotal1"].format(
num=vipPoint
)
if plant:
for key, value in plant.items():
message += config.g_sTranslation["signIn"][
"grandTotal2"
].format(name=key, num=value)
else:
message = config.g_sTranslation["signIn"]["error1"]
pass
return message
@classmethod
def generateSignData(
cls,
todayRewards: list[dict[str, Any]],
cumulativeRewards: list[dict[str, Any]],
currentProgress: int,
totalMilestones: int,
consecutiveDays: int = 0,
) -> dict[str, Any]:
"""生成签到页面所需的数据
Args:
todayRewards (list[dict[str, Any]]): 今日奖励列表
cumulativeRewards (list[dict[str, Any]]): 累计奖励列表
currentProgress (int): 当前进度
totalMilestones (int): 总里程碑数
consecutiveDays (int, optional): 连续签到天数
Returns:
dict[str, Any]: 签到数据字典
"""
# 计算进度百分比
progressPercentage = min(100, int((currentProgress / totalMilestones) * 100))
# 计算已达成里程碑数量
achievedMilestones = 0
for reward in cumulativeRewards:
if reward.get("achieved", False):
achievedMilestones += 1
return {
"currentDate": datetime.now().strftime("%Y年%m月%d日"),
"todayRewards": todayRewards,
"cumulativeRewards": cumulativeRewards,
"currentProgress": currentProgress,
"totalMilestones": totalMilestones,
"progressPercentage": progressPercentage,
"achievedMilestones": achievedMilestones,
"consecutiveDays": consecutiveDays,
}
@classmethod
async def createSignImage(
cls,
todayRewards: list[dict[str, Any]],
cumulativeRewards: list[dict[str, Any]],
currentProgress: int,
totalMilestones: int,
consecutiveDays: int = 0,
):
"""创建签到图片的完整流程
Args:
todayRewards (list[dict[str, Any]]): 今日奖励列表
cumulativeRewards (list[dict[str, Any]]): 累计奖励列表
currentProgress (int): 当前进度
totalMilestones (int): 总里程碑数
consecutiveDays (int, optional): 连续签到天数
"""
try:
templatePath = config.g_sResourcePath / "html/sign_in.html"
outputPath = config.g_sResourcePath / "temp_html/sign_in.html"
savePath = config.DATA_PATH / "farm_res/html/sign_in.png"
# 1. 生成渲染数据
context = cls.generateSignData(
todayRewards=todayRewards,
cumulativeRewards=cumulativeRewards,
currentProgress=currentProgress,
totalMilestones=totalMilestones,
consecutiveDays=consecutiveDays,
)
# 2. 渲染HTML文件
getToolManager().renderHtmlToFile(templatePath, context, outputPath)
# 3. 生成图片字节数据
# imageBytes = await getToolManager().screenshotHtmlToBytes(outputPath)
# 4. 保存图片文件
await getToolManager().screenshotSave(
str(outputPath), str(savePath), 600, 800
)
except Exception as e:
logger.error(f"❌ 生成签到图片时出错: {e}")
raise
@@ -0,0 +1,25 @@
from .database import g_pSqlManager
from .dbServiceLocator import g_pDBlocator
from .plant import CPlantManager
from .user import CUserDB
from .userItem import CUserItemDB
from .userPlant import CUserPlantDB
from .userPlantCount import CUserPlantCountDB
from .userSeed import CUserSeedDB
from .userSign import CUserSignDB
from .userSoil import CUserSoilDB
from .userSteal import CUserStealDB
__all__ = [
"CPlantManager",
"CUserDB",
"CUserItemDB",
"CUserPlantCountDB",
"CUserPlantDB",
"CUserSeedDB",
"CUserSignDB",
"CUserSoilDB",
"CUserStealDB",
"g_pDBlocator",
"g_pSqlManager",
]
@@ -0,0 +1,536 @@
from abc import ABC
from contextlib import asynccontextmanager
import os
from pathlib import Path
import re
from typing import Any, ClassVar
import aiosqlite
from zhenxun.services.log import logger
from ...utils import config
from .dbServiceLocator import g_pDBlocator
class CSqlManager(ABC):
"""
数据库管理器基类
所有具体的数据管理器都应该继承这个类
"""
m_pDB: ClassVar[aiosqlite.Connection | None] = None
def __init__(self):
self.m_sTableName: str | None = None
self.m_bInitialized: bool = False
self.m_pDBPath = Path(config.g_sDBPath)
if self.m_pDBPath and not self.m_pDBPath.exists():
os.makedirs(self.m_pDBPath, exist_ok=True)
@classmethod
async def init(cls) -> bool:
try:
if cls.m_pDB is None:
cls.m_pDB = await aiosqlite.connect(config.g_sDBFilePath)
cls.m_pDB.row_factory = aiosqlite.Row
logger.debug("真寻农场数据库连接初始化成功")
return True
else:
logger.debug("真寻农场数据库连接已存在,跳过初始化")
return True
except Exception as e:
logger.error(f"真寻农场数据库连接初始化失败: {e}")
return False
@classmethod
async def cleanup(cls):
try:
if not cls.isConnectionValid():
logger.warning("真寻农场数据库连接已关闭或无效,跳过清理")
return
if hasattr(cls, "m_pDB") and cls.m_pDB:
await cls.m_pDB.close()
cls.m_pDB = None
cls._initialized = False
logger.debug("真寻农场数据库连接已安全关闭")
else:
logger.debug("真寻农场数据库连接已不存在,无需关闭")
except Exception as e:
logger.error(f"真寻农场数据库清理过程中出错: {e}")
cls.m_pDB = None
cls._initialized = False
@classmethod
def isConnectionValid(cls) -> bool:
if not hasattr(cls, "m_pDB") or cls.m_pDB is None:
return False
try:
return hasattr(cls.m_pDB, "_conn") and cls.m_pDB._conn is not None
except:
return False
@classmethod
def getDB(cls) -> aiosqlite.Connection:
if cls.m_pDB is None:
raise RuntimeError("数据库连接未初始化,请先调用 init_global_db()")
return cls.m_pDB
@classmethod
def getPlantManager(cls) -> Any:
return g_pDBlocator.getPlantManager()
@classmethod
def getUserManager(cls) -> Any:
return g_pDBlocator.getUserManager()
@classmethod
def getUserSoilManager(cls) -> Any:
return g_pDBlocator.getUserSoilManager()
@classmethod
def getUserSeedManager(cls) -> Any:
return g_pDBlocator.getUserSeedManager()
@classmethod
def getUserItemManager(cls) -> Any:
return g_pDBlocator.getUserItemManager()
@classmethod
def getUserStealManager(cls) -> Any:
return g_pDBlocator.getUserStealManager()
@classmethod
def getUserSignManager(cls) -> Any:
return g_pDBlocator.getUserSignManager()
@classmethod
def getUserPlantCountManager(cls) -> Any:
return g_pDBlocator.getUserPlantCountManager()
def isInitialized(self) -> bool:
"""检查是否已初始化"""
return self.m_bInitialized
def setInitialized(self) -> None:
"""标记为已初始化"""
self.m_bInitialized = True
@classmethod
@asynccontextmanager
async def _transaction(cls):
await cls.getDB().execute("BEGIN;")
try:
yield
except:
await cls.getDB().execute("ROLLBACK;")
raise
else:
await cls.getDB().execute("COMMIT;")
@classmethod
async def getTableInfo(cls, tableName: str) -> list:
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", tableName):
raise ValueError(f"Illegal table name: {tableName}")
try:
cursor = await cls.getDB().execute(f'PRAGMA table_info("{tableName}")')
rows = await cursor.fetchall()
return [{"name": row[1], "type": row[2]} for row in rows]
except aiosqlite.Error:
return []
# @classmethod
# async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool:
# """由AI生成
# 创建表或为已存在表添加缺失字段。
# 返回 True 表示有变更(创建或新增列),False 则无操作
# Args:
# tableName (str): 表名
# columns (dict): 字典
# Returns:
# bool:
# """
# info = await cls.getTableInfo(tableName)
# existing = {col["name"]: col["type"].upper() for col in info}
# desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"}
# primaryKey = columns.get("PRIMARY KEY", "")
# if not existing:
# colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
# if primaryKey:
# colsDef += f", PRIMARY KEY {primaryKey}"
# await cls.getDB().execute(f'CREATE TABLE "{tableName}" ({colsDef});')
# return True
# toAdd = [k for k in desired if k not in existing]
# toRemove = [k for k in existing if k not in desired]
# typeMismatch = [
# k for k in desired if k in existing and existing[k] != desired[k]
# ]
# if toAdd and not toRemove and not typeMismatch:
# for col in toAdd:
# await cls.getDB().execute(
# f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}'
# )
# return True
# async with cls._transaction():
# tmpTable = f"{tableName}_new"
# colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
# if primaryKey:
# colsDef += f", PRIMARY KEY {primaryKey}"
# await cls.getDB().execute(f'CREATE TABLE "{tmpTable}" ({colsDef});')
# commonCols = [k for k in desired if k in existing]
# if commonCols:
# colsStr = ", ".join(f'"{c}"' for c in commonCols)
# sql = (
# f'INSERT INTO "{tmpTable}" ({colsStr}) '
# f"SELECT {colsStr} "
# f'FROM "{tableName}";'
# )
# await cls.getDB().execute(sql)
# await cls.getDB().execute(f'DROP TABLE "{tableName}";')
# await cls.getDB().execute(
# f'ALTER TABLE "{tmpTable}" RENAME TO "{tableName}";'
# )
# return True
@classmethod
async def ensureTableSchema(cls, tableName: str, columns: dict) -> bool:
"""由AI生成
创建表或为已存在表添加缺失字段。
返回 True 表示有变更(创建或新增列),False 则无操作
Args:
tableName (str): 表名
columns (dict): 字典
Returns:
bool:
"""
info = await cls.getTableInfo(tableName)
existing = {col["name"]: col["type"].upper() for col in info}
desired = {k: v.upper() for k, v in columns.items() if k != "PRIMARY KEY"}
primaryKey = columns.get("PRIMARY KEY", "")
if not existing:
colsDef = ", ".join(f'"{k}" {v}' for k, v in desired.items())
if primaryKey:
colsDef += f", PRIMARY KEY {primaryKey}"
await cls.getDB().execute(f'CREATE TABLE "{tableName}" ({colsDef});')
return True
toAdd = [k for k in desired if k not in existing]
toRemove = [k for k in existing if k not in desired]
typeMismatch = [
k for k in desired if k in existing and existing[k] != desired[k]
]
if toRemove or typeMismatch:
return False
if toAdd:
for col in toAdd:
try:
await cls.getDB().execute(
f'ALTER TABLE "{tableName}" ADD COLUMN "{col}" {columns[col]}'
)
except aiosqlite.OperationalError as e:
if "duplicate column name" in str(e).lower():
continue
else:
raise
return True
return False
@classmethod
async def executeDB(cls, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if not command:
return False
try:
async with cls._transaction():
await cls.getDB().execute(command)
return True
except Exception:
return False
@classmethod
async def insert(cls, tableName: str, data: dict) -> bool:
"""
插入数据
Args:
tableName: 表名
data: 要插入的数据字典,键为字段名,值为字段值
Returns:
bool: 是否执行成功
"""
if not data:
return False
try:
# 构建参数化查询
columns = ", ".join(f'"{k}"' for k in data.keys())
placeholders = ", ".join("?" for _ in data.keys())
values = list(data.values())
sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})'
async with cls._transaction():
await cls.getDB().execute(sql, values)
return True
except Exception as e:
logger.debug("真寻农场插入数据失败!", e=e)
return False
@classmethod
async def batch_insert(cls, tableName: str, data_list: list) -> bool:
"""
批量插入数据
Args:
tableName: 表名
data_list: 要插入的数据字典列表
Returns:
bool: 是否执行成功
"""
if not data_list:
return False
try:
# 使用第一个字典的键作为所有记录的字段
columns = ", ".join(f'"{k}"' for k in data_list[0].keys())
placeholders = ", ".join("?" for _ in data_list[0].keys())
sql = f'INSERT INTO "{tableName}" ({columns}) VALUES ({placeholders})'
async with cls._transaction():
await cls.getDB().executemany(
sql, [list(data.values()) for data in data_list]
)
return True
except Exception as e:
logger.debug("真寻农场批量插入数据失败!", e=e)
return False
@classmethod
async def select(
cls,
tableName: str,
columns: list[Any] | None = None,
where: dict[str, Any] | None = None,
order_by: str | None = None,
limit: int | None = None,
) -> list[dict]:
"""
查询数据
Args:
tableName: 表名
columns: 要查询的字段列表,None表示所有字段
where: 查询条件字典
order_by: 排序字段
limit: 限制返回记录数
Returns:
list: 查询结果列表,每个元素是一个字典
"""
try:
# 构建SELECT部分
if columns:
select_clause = ", ".join(f'"{col}"' for col in columns)
else:
select_clause = "*"
sql = f'SELECT {select_clause} FROM "{tableName}"'
# 构建WHERE部分
params = []
if where:
where_conditions = []
for key, value in where.items():
if isinstance(value, (list, tuple)):
# 处理IN查询
placeholders = ", ".join("?" for _ in value)
where_conditions.append(f'"{key}" IN ({placeholders})')
params.extend(value)
else:
where_conditions.append(f'"{key}" = ?')
params.append(value)
if where_conditions:
sql += " WHERE " + " AND ".join(where_conditions)
# 构建ORDER BY部分
if order_by:
sql += f" ORDER BY {order_by}"
# 构建LIMIT部分
if limit:
sql += f" LIMIT {limit}"
cursor = await cls.getDB().execute(sql, params)
rows = await cursor.fetchall()
# 转换为字典列表
result = []
for row in rows:
result.append(dict(row))
return result
except Exception as e:
logger.debug("真寻农场查询数据失败!", e=e)
return []
@classmethod
async def update(cls, tableName: str, data: dict, where: dict) -> bool:
"""
更新数据
Args:
tableName: 表名
data: 要更新的数据字典
where: 更新条件字典
Returns:
bool: 是否执行成功
"""
if not data:
return False
if not where:
return False
try:
# 构建SET部分
set_conditions = []
params = []
for key, value in data.items():
set_conditions.append(f'"{key}" = ?')
params.append(value)
# 构建WHERE部分
where_conditions = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql = f'UPDATE "{tableName}" SET {", ".join(set_conditions)} WHERE {" AND ".join(where_conditions)}'
async with cls._transaction():
cursor = await cls.getDB().execute(sql, params)
# 检查是否影响了行
return cursor.rowcount > 0
except Exception as e:
logger.debug("真寻农场更新数据失败!", e=e)
return False
@classmethod
async def delete(cls, tableName: str, where: dict) -> bool:
"""
删除数据
Args:
tableName: 表名
where: 删除条件字典
Returns:
bool: 是否执行成功
"""
if not where:
return False
try:
# 构建WHERE部分
where_conditions = []
params = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql = f'DELETE FROM "{tableName}" WHERE {" AND ".join(where_conditions)}'
async with cls._transaction():
cursor = await cls.getDB().execute(sql, params)
# 检查是否影响了行
return cursor.rowcount > 0
except Exception as e:
logger.debug("真寻农场删除数据失败!", e=e)
return False
@classmethod
async def exists(cls, tableName: str, where: dict) -> bool:
"""
检查记录是否存在
Args:
tableName: 表名
where: 查询条件字典
Returns:
bool: 是否存在符合条件的记录
"""
try:
result = await cls.select(tableName, columns=["1"], where=where, limit=1)
return len(result) > 0
except Exception as e:
logger.debug("真寻农场检查数据失败!", e=e)
return False
@classmethod
async def count(cls, tableName: str, where: dict = {}) -> int:
"""
统计记录数量
Args:
tableName: 表名
where: 查询条件字典
Returns:
int: 记录数量
"""
try:
# 构建WHERE部分
sql = f'SELECT COUNT(*) as count FROM "{tableName}"'
params = []
if where:
where_conditions = []
for key, value in where.items():
where_conditions.append(f'"{key}" = ?')
params.append(value)
sql += " WHERE " + " AND ".join(where_conditions)
cursor = await cls.getDB().execute(sql, params)
row = await cursor.fetchone()
return row["count"] if row else 0
except Exception as e:
logger.debug("真寻农场统计数据失败!", e=e)
return 0
g_pSqlManager = CSqlManager()
@@ -0,0 +1,85 @@
from typing import Any, Optional
from zhenxun.services.log import logger
class CDBServiceLocator:
"""
数据库服务定位器
负责管理所有数据库服务实例,提供统一的访问接口
"""
m_pInstance: Optional["CDBServiceLocator"] = None
def __init__(self):
self.m_pServices: dict[str, Any] = {}
self.m_bInitialized = False
@classmethod
def getInstance(cls) -> "CDBServiceLocator":
"""
获取单例实例
"""
if cls.m_pInstance is None:
cls.m_pInstance = cls()
return cls.m_pInstance
def register(self, name: str, service: Any) -> None:
"""
注册服务实例
"""
if name in self.m_pServices:
logger.error(f"真寻农场服务 {name} 已经注册")
self.m_pServices[name] = service
def get(self, name: str) -> Any:
"""
获取服务实例
"""
if name not in self.m_pServices:
raise KeyError(f"服务 {name} 未注册")
return self.m_pServices[name]
def getPlantManager(self):
return self.get("plant")
def getUserManager(self):
return self.get("user")
def getUserSoilManager(self):
return self.get("userSoil")
def getUserPlantManager(self):
return self.get("userPlant")
def getUserSeedManager(self):
return self.get("userSeed")
def getUserItemManager(self):
return self.get("userItem")
def getUserStealManager(self):
return self.get("userSteal")
def getUserSignManager(self):
return self.get("userSign")
def getUserPlantCountManager(self):
return self.get("userPlantCount")
def markInitialized(self):
"""
标记初始化完成
"""
self.m_bInitialized = True
@property
def initialized(self) -> bool:
"""
检查是否已初始化
"""
return self.m_bInitialized
# 全局服务定位器实例
g_pDBlocator = CDBServiceLocator.getInstance()
@@ -0,0 +1,290 @@
from contextlib import asynccontextmanager
import os
import aiosqlite
from zhenxun.configs.config import Config
from zhenxun.services.log import logger
from ...utils import config, getRequestManager
class CPlantManager:
def __init__(self):
self.m_sTableName = "plant"
self.m_bInitialized = False
try:
os.mkdir(config.g_sPlantPath)
except FileExistsError:
pass
async def cleanup(self):
if hasattr(self, "m_pDB") and self.m_pDB:
await self.m_pDB.close()
async def init(self) -> bool:
try:
_ = os.path.exists(config.g_sPlantPath)
if config.g_bIsDebug:
self.m_pDB = await aiosqlite.connect(
str(config.g_sPlantPath.parent / "plant-test.db")
)
else:
self.m_pDB = await aiosqlite.connect(str(config.g_sPlantPath))
self.m_pDB.row_factory = aiosqlite.Row
return True
except Exception as e:
logger.warning("初始化植物数据库失败", e=e)
return False
async def initDB(self):
self.m_bInitialized = True
@asynccontextmanager
async def _transaction(self):
await self.m_pDB.execute("BEGIN;")
try:
yield
except:
await self.m_pDB.execute("ROLLBACK;")
raise
else:
await self.m_pDB.execute("COMMIT;")
async def executeDB(self, command: str) -> bool:
"""执行自定义SQL
Args:
command (str): SQL语句
Returns:
bool: 是否执行成功
"""
if not command:
logger.warning("数据库语句长度为空!")
return False
try:
async with self._transaction():
await self.m_pDB.execute(command)
return True
except Exception as e:
logger.warning(f"数据库语句执行出错: {command}", e=e)
return False
async def getPlantByName(self, name: str) -> dict | None:
"""根据作物名称查询记录
Args:
name (str): 作物名称
Returns:
dict | None: 返回记录字典,未找到返回None
"""
try:
async with self.m_pDB.execute(
"SELECT * FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
return dict(row) if row else None
except Exception as e:
logger.warning(f"查询作物失败: {name}", e=e)
return None
async def getPlantPhaseByName(self, name: str) -> list[int]:
"""根据作物名称获取作物各个阶段
Args:
name (str): 作物名称
Returns:
list: 阶段数组
"""
try:
async with self.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return []
phase = row[0].split(",")
seen = set()
result = []
for x in phase:
num = int(x)
if num not in seen:
seen.add(num)
result.append(num)
return result
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return []
async def getPlantPhaseNumberByName(self, name: str) -> int:
"""根据作物名称获取作物总阶段数
Args:
name (str): 作物名称
Returns:
int: 总阶段数
"""
try:
async with self.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return -1
phase = row[0].split(",")
# 去重
seen = set()
result = []
for x in phase:
if x not in seen:
seen.add(x)
result.append(x)
return len(result)
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return -1
async def getPlantAgainByName(self, name: str) -> int:
"""根据作物名称获取作物再次成熟时间
Args:
name (str): 作物名称
Returns:
int: 再次成熟时间 单位:h
"""
try:
async with self.m_pDB.execute(
"SELECT phase FROM plant WHERE name = ?", (name,)
) as cursor:
row = await cursor.fetchone()
if not row:
return -1
phase = row[0].split(",")
again = phase[-1] - phase[3] / 60 / 60
return again
except Exception as e:
logger.warning(f"查询作物阶段失败: {name}", e=e)
return -1
async def existsPlant(self, name: str) -> bool:
"""判断作物是否存在
Args:
name (str): 作物名称
Returns:
bool: 存在返回True,否则False
"""
try:
async with self.m_pDB.execute(
"SELECT 1 FROM plant WHERE name = ? LIMIT 1", (name,)
) as cursor:
row = await cursor.fetchone()
return True if row else False
except Exception as e:
logger.warning(f"检查作物存在性失败: {name}", e=e)
return False
async def countPlants(self, onlyBuy: bool = False) -> int:
"""获取作物总数
Args:
onlyBuy (bool): 若为True,仅统计isBuy=1的记录,默认False
Returns:
int: 符合条件的记录数
"""
try:
if onlyBuy:
sql = "SELECT COUNT(*) FROM plant WHERE isBuy = 1"
params: tuple = ()
else:
sql = "SELECT COUNT(*) FROM plant"
params: tuple = ()
async with self.m_pDB.execute(sql, params) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.warning(f"统计作物数量失败, onlyBuy={onlyBuy}", e=e)
return 0
async def listPlants(self) -> list[dict]:
"""查询所有作物记录"""
try:
async with self.m_pDB.execute(
"SELECT * FROM plant ORDER BY level"
) as cursor:
rows = await cursor.fetchall()
return [dict(r) for r in rows]
except Exception as e:
logger.warning("查询所有作物失败", e=e)
return []
async def downloadPlant(self) -> bool:
"""遍历所有作物,下载各阶段图片及icon文件到指定文件夹
Returns:
bool: 全部下载完成返回True,如有失败返回False
"""
success = True
baseUrl = Config.get_config("zhenxun_plugin_farm", "服务地址")
baseUrl = baseUrl.rstrip("/") + ":8998/file"
try:
plants = await self.listPlants()
for plant in plants:
name = plant["name"]
phaseCount = await self.getPlantPhaseNumberByName(name)
saveDir = os.path.join(config.g_sResourcePath, "plant", name)
begin = 0 if plant["general"] == 0 else 1
for idx in range(begin, phaseCount + 1):
fileName = f"{idx}.png"
fullPath = os.path.join(saveDir, fileName)
if os.path.exists(fullPath):
continue
url = f"{baseUrl}/{name}/{idx}.png"
if not await getRequestManager().download(
url, saveDir, f"{idx}.png"
):
success = False
iconName = "icon.png"
iconPath = os.path.join(saveDir, iconName)
if not os.path.exists(iconPath):
iconUrl = f"{baseUrl}/{name}/{iconName}"
if not await getRequestManager().download(
iconUrl, saveDir, iconName
):
success = False
return success
except Exception as e:
logger.warning(f"下载作物资源异常: {e}")
return False
@@ -0,0 +1,214 @@
import math
from ...utils import getToolManager
from .database import CSqlManager
class CUserDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "user"
self.currencies: list[str] = ["point", "vipPoint"]
async def initDB(self):
userInfo = {
"uid": "TEXT PRIMARY KEY", # 用户Uid
"name": "TEXT NOT NULL", # 农场名称
"exp": "INTEGER DEFAULT 0", # 经验值
"point": "INTEGER DEFAULT 0", # 金币
"vipPoint": "INTEGER DEFAULT 0", # 点券
"soil": "INTEGER DEFAULT 3", # 解锁土地数量
"stealTime": "TEXT DEFAULT ''", # 偷菜时间字符串
"stealCount": "INTEGER DEFAULT 0", # 剩余偷菜次数
}
await self.ensureTableSchema("user", userInfo)
self.setInitialized()
async def initUserInfo(self, uid: str, name: str) -> bool:
"""初始化用户信息
Args:
uid (str): 用户ID
name (str): 农场名称
Returns:
bool: 是否成功初始化用户信息
"""
nowStr = getToolManager().dateTime().date().today().strftime("%Y-%m-%d")
result = await self.insert(
"user",
data={
"uid": uid,
"name": name,
"exp": 0,
"point": 500,
"soil": 3,
"stealTime": nowStr,
"stealCount": 5,
},
)
return result
async def getUserInfoByUid(self, uid: str) -> dict:
"""根据用户ID获取用户信息
Args:
uid (str): 用户ID
Returns:
dict: 用户信息字典,未找到返回空字典
"""
if uid == "":
return {}
records = await self.select("user", where={"uid": uid})
return records[0] if records else {}
async def isRegistered(self, uid: str) -> bool:
"""检查用户是否注册农场
Args:
uid (str): 用户ID
Returns:
bool: 是否注册农场
"""
if uid == "":
return False
return await self.exists("user", where={"uid": uid})
async def updatePoint(self, uid: str, type: str, index: int) -> bool:
"""更新货币
Args:
uid (str): 用户ID
type (str): 货币类型 point/vipPoint
index (int): 更新后的数量
Returns:
bool: 是否成功更新货币
"""
if type not in self.currencies:
return False
if index < 0:
index = 0
await self.update("user", {type: index}, {"uid": uid})
return True
async def updateExp(self, uid: str, exp: int) -> bool:
"""更新经验值
Args:
uid (str): 用户ID
exp (int): 更新后的经验值
Returns:
bool: 是否成功更新经验值
"""
if exp < 0:
exp = 0
return await self.update("user", {"exp": exp}, {"uid": uid})
async def updateName(self, uid: str, name: str) -> str:
"""更新农场名称
Args:
uid (str): 用户ID
name (str): 农场名称
Returns:
bool: 是否成功更新农场名称
"""
safeName = getToolManager().sanitize_username(name)
if safeName == "神秘农夫":
return "error"
if await self.update("user", {"name": safeName}, {"uid": uid}):
return "success"
return "error1"
async def getUserLevelByUid(self, uid: str) -> tuple[int, int, int]:
"""获取用户等级信息
Args:
uid (str): 用户Uid
Returns:
tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验)
失败返回(-1, -1, -1)
"""
if not uid:
return -1, -1, -1
records = await self.select("user", where={"uid": uid}, columns=["exp"])
if not records:
return -1, -1, -1
try:
exp = int(records[0].get("exp", 0))
except Exception:
exp = 0
levelStep = 200 # 每级经验增量
discriminant = 1 + 8 * exp / levelStep
level = int((-1 + math.sqrt(discriminant)) // 2)
if level < 0:
level = 0
def cumExp(k: int) -> int:
return levelStep * k * (k + 1) // 2
totalExpCurrentLevel = cumExp(level)
totalExpNextLevel = cumExp(level + 1)
currentExp = exp - totalExpCurrentLevel
return level, totalExpNextLevel, currentExp
async def updateStealCountByUid(
self, uid: str, stealTime: str, stealCount: int
) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
stealTime (str): 偷菜日期
stealCount (int): 新剩余偷菜次数
Returns:
bool: 是否更新成功
"""
if not uid or stealCount < 0:
return False
return await self.update(
"user", {"stealTime": stealTime, "stealCount": stealCount}, {"uid": uid}
)
async def updateFieldByUid(self, uid: str, field: str, value) -> bool:
"""更新单字段信息
Args:
uid (str): 用户Uid
field (str): 字段名称
Returns:
bool: 是否成功更新字段
"""
if not uid or not field:
return False
return await self.update("user", {field: value}, {"uid": uid})
@@ -0,0 +1,162 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserItemDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userItem"
async def initDB(self):
userItem = {
"uid": "TEXT NOT NULL", # 用户Uid
"item": "TEXT NOT NULL", # 物品名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, item)",
}
await self.ensureTableSchema("userItem", userItem)
self.setInitialized()
@classmethod
async def getUserItemByName(cls, uid: str, item: str) -> int | None:
"""根据道具名称查询某一项数量
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
Optional[int]: 数量(不存在返回None)
"""
if not uid or not item:
return None
try:
async with cls.getDB().execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?", (uid, item)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning("getUserItemByName查询失败!", e=e)
return None
@classmethod
async def getUserItemByUid(cls, uid: str) -> dict:
"""根据用户Uid获取全部道具信息
Args:
uid (str): 用户uid
Returns:
dict: {itemName: count, ...}
"""
if not uid:
return {}
try:
cursor = await cls.getDB().execute(
"SELECT item, count FROM userItem WHERE uid = ?", (uid,)
)
rows = await cursor.fetchall()
return {row["item"]: row["count"] for row in rows}
except Exception as e:
logger.warning("getUserItemByUid查询失败!", e=e)
return {}
@classmethod
async def deleteUserItemByName(cls, uid: str, item: str) -> bool:
"""根据道具名删除道具
Args:
uid (str): 用户uid
item (str): 道具名称
Returns:
bool: 是否删除成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
await cls.getDB().execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?", (uid, item)
)
return True
except Exception as e:
logger.warning("deleteUserItemByName失败!", e=e)
return False
@classmethod
async def updateUserItemByName(cls, uid: str, item: str, count: int) -> bool:
"""根据道具名直接更新道具数量
Args:
uid (str): 用户uid
item (str): 道具名称
count (int): 要更新的新数量
Returns:
bool: 是否更新成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
if count <= 0:
await cls.getDB().execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?", (uid, item)
)
else:
await cls.getDB().execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(count, uid, item),
)
return True
except Exception as e:
logger.warning("updateUserItemByName失败!", e=e)
return False
@classmethod
async def addUserItemByUid(cls, uid: str, item: str, count: int = 1) -> bool:
"""根据用户uid添加道具信息
Args:
uid (str): 用户uid
item (str): 道具名称
count (int, optional): 数量.Defaults to 1.
Returns:
bool: 是否添加成功
"""
if not uid or not item:
return False
try:
async with cls._transaction():
async with cls.getDB().execute(
"SELECT count FROM userItem WHERE uid = ? AND item = ?", (uid, item)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
if newCount <= 0:
await cls.getDB().execute(
"DELETE FROM userItem WHERE uid = ? AND item = ?",
(uid, item),
)
else:
await cls.getDB().execute(
"UPDATE userItem SET count = ? WHERE uid = ? AND item = ?",
(newCount, uid, item),
)
else:
if count > 0:
await cls.getDB().execute(
"INSERT INTO userItem (uid, item, count) VALUES (?, ?, ?)",
(uid, item, count),
)
return True
except Exception as e:
logger.warning("addUserItemByUid失败!", e=e)
return False
@@ -0,0 +1,209 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserPlantDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userPlant"
async def initDB(self):
userPlant = {
"uid": "TEXT NOT NULL", # 用户Uid
"plant": "TEXT NOT NULL", # 作物名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"isLock": "INTEGER NOT NULL DEFAULT 0", # 是否上锁 0=没有,非0=有
"PRIMARY KEY": "(uid, plant)",
}
await self.ensureTableSchema("userPlant", userPlant)
self.setInitialized()
@classmethod
async def addUserPlantByUid(cls, uid: str, plant: str, count: int = 1) -> bool:
"""根据用户uid添加作物信息
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
# 检查是否已存在该作物
async with cls.getDB().execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?",
(uid, plant),
) as cursor:
row = await cursor.fetchone()
if row:
# 如果作物已存在,则更新数量
new_count = row[0] + count
await cls.getDB().execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(new_count, uid, plant),
)
else:
# 如果作物不存在,则插入新记录
await cls.getDB().execute(
"INSERT INTO userPlant (uid, plant, count) VALUES (?, ?, ?)",
(uid, plant, count),
)
return True
except Exception as e:
logger.warning("addUserPlantByUid 失败!", e=e)
return False
@classmethod
async def getUserPlantByUid(cls, uid: str) -> dict[str, int]:
"""根据用户uid获取全部作物信息
Args:
uid (str): 用户uid
Returns:
Dict[str, int]: 作物名称和数量
"""
cursor = await cls.getDB().execute(
"SELECT plant, count FROM userPlant WHERE uid=?", (uid,)
)
rows = await cursor.fetchall()
return {row["plant"]: row["count"] for row in rows}
@classmethod
async def getUserPlantByName(cls, uid: str, plant: str) -> int | None:
"""根据作物名称获取用户的作物数量
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
Optional[int]: 作物数量
"""
try:
async with cls.getDB().execute(
"SELECT count FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning("getUserPlantByName 查询失败!", e=e)
return None
@classmethod
async def checkUserPlantByName(cls, uid: str, plant: str) -> bool:
"""根据作物名称判断用户作物仓库是否存在该作物
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否存在
"""
try:
async with cls.getDB().execute(
"SELECT * FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return True if row else False
except Exception as e:
logger.warning("checkUserPlantByName 查询失败!", e=e)
return False
@classmethod
async def updateUserPlantByName(cls, uid: str, plant: str, count: int) -> bool:
"""更新 userPlant 表中某个作物的数量
Args:
uid (str): 用户uid
plant (str): 作物名称
count (int): 新的作物数量
Returns:
bool: 是否更新成功
"""
try:
if count <= 0:
return await cls.deleteUserPlantByName(uid, plant)
async with cls._transaction():
await cls.getDB().execute(
"UPDATE userPlant SET count = ? WHERE uid = ? AND plant = ?",
(count, uid, plant),
)
return True
except Exception as e:
logger.warning("updateUserPlantByName 失败!", e=e)
return False
@classmethod
async def lockUserPlantByName(cls, uid: str, plant: str, lock: int) -> bool:
"""给作物加锁,防止一键售卖
Args:
uid (str): 用户uid
plant (str): 作物名称
lock (int): 0为解锁,非0均为加锁
Returns:
bool: 是否加锁成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
"UPDATE userPlant SET isLock = ? WHERE uid = ? AND plant = ?",
(lock, uid, plant),
)
return True
except Exception as e:
logger.warning("lockUserPlantByName 失败!", e=e)
return False
@classmethod
async def checkPlantLockByName(cls, uid: str, plant: str) -> bool:
"""根据作物名称判断是否加锁
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否加锁
"""
try:
async with cls.getDB().execute(
"SELECT isLock FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
) as cursor:
row = await cursor.fetchone()
return row[0] > 0 if row else False
except Exception as e:
logger.warning("checkUserPlantByName 查询失败!", e=e)
return False
@classmethod
async def deleteUserPlantByName(cls, uid: str, plant: str) -> bool:
"""从 userPlant 表中删除某个作物记录
Args:
uid (str): 用户uid
plant (str): 作物名称
Returns:
bool: 是否删除成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
"DELETE FROM userPlant WHERE uid = ? AND plant = ?", (uid, plant)
)
return True
except Exception as e:
logger.warning("deleteUserPlantByName 失败!", e=e)
return False
@@ -0,0 +1,146 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserPlantCountDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userPlantCount"
async def initDB(self):
userPlantCount = {
"uid": "TEXT NOT NULL", # 用户Uid
"plant": "TEXT NOT NULL", # 植物名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, plant)",
}
await self.ensureTableSchema("userPlantCount", userPlantCount)
self.setInitialized()
@classmethod
async def addUserPlantCountByUid(cls, uid: str, plant: str, count: int = 1) -> bool:
"""根据用户uid添加植物的收获次数
Args:
uid (str): 用户uid
plant (str): 植物名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
async with cls.getDB().execute(
"SELECT count FROM userPlantCount WHERE uid = ? AND plant = ?",
(uid, plant),
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
await cls.getDB().execute(
"UPDATE userPlantCount SET count = ? WHERE uid = ? AND plant = ?",
(newCount, uid, plant),
)
else:
newCount = count
await cls.getDB().execute(
"INSERT INTO userPlantCount (uid, plant, count) VALUES (?, ?, ?)",
(uid, plant, count),
)
if newCount <= 0:
await cls.getDB().execute(
"DELETE FROM userPlantCount WHERE uid = ? AND plant = ?",
(uid, plant),
)
return True
except Exception as e:
logger.warning("addUserPlantByUid 失败!", e=e)
return False
@classmethod
async def getUserPlantCountByUid(cls, uid: str) -> dict:
"""根据用户Uid获取该用户的所有植物的收获次数
Args:
uid (str): 用户uid
Returns:
dict: 植物信息
"""
cursor = await cls.getDB().execute(
"SELECT plant, count FROM userPlantCount WHERE uid=?", (uid,)
)
rows = await cursor.fetchall()
return {row["plant"]: row["count"] for row in rows}
@classmethod
async def getUserPlantCountByPlantName(cls, uid: str, plant: str) -> int:
"""根据用户Uid和植物名称获取该对应的收获次数
Args:
uid (str): 用户uid
plant (str): 植物名称
Returns:
int: 收获次数
"""
cursor = await cls.getDB().execute(
"SELECT count FROM userPlantCount WHERE uid = ? AND plant = ?",
(uid, plant),
)
row = await cursor.fetchone()
return row["count"] if row else 0
@classmethod
async def updateUserPlantCountByName(cls, uid: str, plant: str, count: int) -> bool:
"""根据植物名称更新植物的收获次数
Args:
uid (str): 用户uid
plant (str): 植物名称
count (int): 收获次数
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserPlantCountByName(uid, plant)
async with cls._transaction():
await cls.getDB().execute(
"UPDATE userPlantCount SET count = ? WHERE uid = ? AND plant = ?",
(count, uid, plant),
)
return True
except Exception as e:
logger.warning("updateUserPlantByName失败!", e=e)
return False
@classmethod
async def deleteUserPlantCountByName(cls, uid: str, plant: str) -> bool:
"""根据植物名称从植物仓库中删除植物
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
"DELETE FROM userPlantCount WHERE uid = ? AND plant = ?",
(uid, plant),
)
return True
except Exception as e:
logger.warning("deleteUserPlantCountByName 删除失败!", e=e)
return False
@@ -0,0 +1,216 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserSeedDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userSeed"
async def initDB(self):
userSeed = {
"uid": "TEXT NOT NULL", # 用户Uid
"seed": "TEXT NOT NULL", # 种子名称
"count": "INTEGER NOT NULL DEFAULT 0", # 数量
"PRIMARY KEY": "(uid, seed)",
}
await self.ensureTableSchema("userSeed", userSeed)
self.setInitialized()
@classmethod
async def addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息(事务版本)
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 数量
Returns:
bool: 是否添加成功
"""
try:
async with cls._transaction():
async with cls.getDB().execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
) as cursor:
row = await cursor.fetchone()
if row:
newCount = row[0] + count
await cls.getDB().execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(newCount, uid, seed),
)
else:
newCount = count
await cls.getDB().execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, count),
)
if newCount <= 0:
await cls.getDB().execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning("addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def _addUserSeedByUid(cls, uid: str, seed: str, count: int = 1) -> bool:
"""根据用户uid添加种子信息(非事务版,复用其他非事务接口)"""
try:
existing = await cls.getUserSeedByName(uid, seed)
newCount = (existing or 0) + count
if existing is not None:
await cls._updateUserSeedByName(uid, seed, newCount)
else:
await cls.getDB().execute(
"INSERT INTO userSeed (uid, seed, count) VALUES (?, ?, ?)",
(uid, seed, newCount),
)
if newCount <= 0:
await cls._deleteUserSeedByName(uid, seed)
return True
except Exception as e:
logger.warning("_addUserSeedByUid 失败!", e=e)
return False
@classmethod
async def getUserSeedByName(cls, uid: str, seed: str) -> int | None:
"""根据种子名称获取种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
Optional[int]: 种子数量
"""
try:
async with cls.getDB().execute(
"SELECT count FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
) as cursor:
row = await cursor.fetchone()
return row[0] if row else None
except Exception as e:
logger.warning("getUserSeedByName 查询失败!", e=e)
return None
@classmethod
async def getUserSeedByUid(cls, uid: str) -> dict:
"""根据用户Uid获取仓库全部种子信息
Args:
uid (str): 用户uid
Returns:
dict: 种子信息
"""
cursor = await cls.getDB().execute(
"SELECT seed, count FROM userSeed WHERE uid=?", (uid,)
)
rows = await cursor.fetchall()
return {row["seed"]: row["count"] for row in rows}
@classmethod
async def updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.getDB().execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed),
)
return True
except Exception as e:
logger.warning("updateUserSeedByName失败!", e=e)
return False
@classmethod
async def _updateUserSeedByName(cls, uid: str, seed: str, count: int) -> bool:
"""根据种子名称更新种子数量
Args:
uid (str): 用户uid
seed (str): 种子名称
count (int): 种子数量
Returns:
bool: 是否成功
"""
try:
if count <= 0:
return await cls.deleteUserSeedByName(uid, seed)
async with cls._transaction():
await cls.getDB().execute(
"UPDATE userSeed SET count = ? WHERE uid = ? AND seed = ?",
(count, uid, seed),
)
return True
except Exception as e:
logger.warning("updateUserSeedByName失败!", e=e)
return False
@classmethod
async def deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning("deleteUserSeedByName 删除失败!", e=e)
return False
@classmethod
async def _deleteUserSeedByName(cls, uid: str, seed: str) -> bool:
"""根据种子名称从种子仓库中删除种子
Args:
uid (str): 用户uid
seed (str): 种子名称
Returns:
bool: 是否成功
"""
try:
await cls.getDB().execute(
"DELETE FROM userSeed WHERE uid = ? AND seed = ?", (uid, seed)
)
return True
except Exception as e:
logger.warning("deleteUserSeedByName 删除失败!", e=e)
return False
@@ -0,0 +1,293 @@
import calendar
from datetime import timedelta
import random
from zhenxun.services.log import logger
from zhenxun.utils._build_image import BuildImage
from ...utils import config, getJsonManager, getToolManager
from .database import CSqlManager
class CUserSignDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userSign"
async def initDB(self):
# userSignLog 表结构,每条为一次签到事件
userSignLog = {
"uid": "TEXT NOT NULL", # 用户ID
"signDate": "DATE NOT NULL", # 签到日期
"isSupplement": "TINYINT NOT NULL DEFAULT 0", # 是否补签
"exp": "INT NOT NULL DEFAULT 0", # 当天签到经验
"point": "INT NOT NULL DEFAULT 0", # 当天签到金币
"createdAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))", # 创建时间 # noqa: E501
"PRIMARY KEY": "(uid, signDate)",
}
# userSignSummary 表结构,每用户一行用于缓存签到状态
userSignSummary = {
"uid": "TEXT PRIMARY KEY NOT NULL", # 用户ID
"totalSignDays": "INT NOT NULL DEFAULT 0", # 累计签到天数
"currentMonth": "CHAR(7) NOT NULL DEFAULT ''", # 当前月份(如2025-05)
"monthSignDays": "INT NOT NULL DEFAULT 0", # 本月签到次数
"lastSignDate": "DATE DEFAULT NULL", # 上次签到日期
"continuousDays": "INT NOT NULL DEFAULT 0", # 连续签到天数
"supplementCount": "INT NOT NULL DEFAULT 0", # 补签次数
"updatedAt": "DATETIME NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime'))", # 更新时间 # noqa: E501
}
await self.ensureTableSchema("userSignLog", userSignLog)
await self.ensureTableSchema("userSignSummary", userSignSummary)
self.setInitialized()
@classmethod
async def getUserSignRewardByDate(cls, uid: str, date: str) -> tuple[int, int]:
"""根据指定日期获取用户签到随机奖励
Args:
uid (str): 用户Uid
date (str): 用户签到日期 示例:2025-05-27
Returns:
tuple[int, int]: 经验、金币
"""
try:
async with cls._transaction():
async with cls.getDB().execute(
"SELECT exp, point FROM userSignLog WHERE uid=? AND signDate=?",
(uid, date),
) as cursor:
row = await cursor.fetchone()
if row is None:
return 0, 0
exp = row["exp"]
point = row["point"]
return exp, point
except Exception as e:
logger.warning("获取用户签到数据失败", e=e)
return 0, 0
@classmethod
async def getUserSignCountByDate(cls, uid: str, monthStr: str) -> int:
"""根据日期查询用户签到总天数
Args:
uid (str): 用户Uid
monthStr (str): 需要查询的日期 示例: 2025-05
Returns:
int: 查询月总签到天数
"""
try:
sql = "SELECT COUNT(*) FROM userSignLog WHERE uid=? AND signDate LIKE ?"
param = f"{monthStr}-%"
async with cls.getDB().execute(sql, (uid, param)) as cursor:
row = await cursor.fetchone()
return row[0] if row else 0
except Exception as e:
logger.warning("统计用户月签到次数失败", e=e)
return 0
@classmethod
async def hasSigned(cls, uid: str, signDate: str) -> bool:
"""判断指定日期是否已签到
Args:
uid (int): 用户ID
signDate (str): 日期字符串 'YYYY-MM-DD'
Returns:
bool: True=已签到,False=未签到
"""
try:
sql = "SELECT 1 FROM userSignLog WHERE uid=? AND signDate=? LIMIT 1"
async with cls.getDB().execute(sql, (uid, signDate)) as cursor:
row = await cursor.fetchone()
return row is not None
except Exception as e:
logger.warning("查询是否已签到失败", e=e)
return False
@classmethod
async def sign(cls, uid: str, signDate: str = "") -> int:
"""签到
Args:
uid (int): 用户ID
signDate (str): 日期字符串 'YYYY-MM-DD' 不传默认当前系统日期
Returns:
int: 0: 签到失败 1: 签到成功 2: 重复签到
"""
try:
player = await getToolManager().getPlayerByUid(uid)
if not player:
return 0
if not signDate:
signDate = (
getToolManager().dateTime().date().today().strftime("%Y-%m-%d")
)
if await cls.hasSigned(uid, signDate):
return 2
todayStr = getToolManager().dateTime().date().today().strftime("%Y-%m-%d")
isSupplement = 0 if signDate == todayStr else 1
sign = await getJsonManager().getSign()
expMax, expMin, pointMax, pointMin = [
sign.get(key, default)
for key, default in (
("exp_max", 50),
("exp_min", 5),
("point_max", 2000),
("point_min", 200),
)
]
exp = random.randint(expMin, expMax)
point = random.randint(pointMin, pointMax)
vipPoint = 0
async with cls._transaction():
await cls.getDB().execute(
"INSERT INTO userSignLog (uid, signDate, isSupplement, exp, point) VALUES (?, ?, ?, ?, ?)",
(uid, signDate, isSupplement, exp, point),
)
cursor = await cls.getDB().execute(
"SELECT * FROM userSignSummary WHERE uid=?", (uid,)
)
row = await cursor.fetchone()
currentMonth = signDate[:7]
if row:
monthSignDays = (
row["monthSignDays"] + 1
if row["currentMonth"] == currentMonth
else 1
)
prevDate = (
getToolManager().dateTime().strptime(signDate, "%Y-%m-%d")
- timedelta(days=1)
).strftime("%Y-%m-%d")
continuousDays = (
row["continuousDays"] + 1
if row["lastSignDate"] == prevDate
else 1
)
supplementCount = (
row["supplementCount"] + 1
if isSupplement
else row["supplementCount"]
)
await cls.getDB().execute(
"""
UPDATE userSignSummary
SET totalSignDays=totalSignDays+1,
currentMonth=?,
monthSignDays=?,
lastSignDate=?,
continuousDays=?,
supplementCount=?
WHERE uid=?
""",
(
currentMonth,
monthSignDays,
signDate,
continuousDays,
supplementCount,
uid,
),
)
else:
monthSignDays = 1
await cls.getDB().execute(
"""
INSERT INTO userSignSummary
(uid, totalSignDays, currentMonth, monthSignDays, lastSignDate, continuousDays, supplementCount)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
uid,
1,
currentMonth,
monthSignDays,
signDate,
1,
1 if isSupplement else 0,
),
)
# 计算累签奖励
reward = sign["continuou"].get(f"{monthSignDays}", None)
if reward:
point += reward.get("point", 0)
exp += reward.get("exp", 0)
vipPoint = reward.get("vipPoint", 0)
plant = reward.get("plant", {})
if plant:
for key, value in plant.items():
await cls.getUserSeedManager().addUserSeedByUid(uid, key, value)
if config.g_bIsDebug:
exp += 9999
# 向数据库更新
await player.addExp(exp)
await player.addPoint("point", point)
if vipPoint > 0:
await player.addPoint("vipPoint", vipPoint)
return 1
except Exception as e:
logger.warning("执行签到失败", e=e)
return 0
@classmethod
async def drawSignCalendarImage(cls, uid: str, year: int, month: int):
# 绘制签到图,自动提取数据库中该用户该月的签到天数
cellSize = 80
padding = 40
titleHeight = 80
cols = 7
rows = 6
width = cellSize * cols + padding * 2
height = cellSize * rows + padding * 2 + titleHeight
img = BuildImage(width, height, color=(255, 255, 255))
await img.text((padding, 20), f"{year}年{month}月签到表", font_size=36)
firstWeekday, totalDays = calendar.monthrange(year, month)
monthStr = f"{year:04d}-{month:02d}"
try:
sql = "SELECT signDate FROM userSignLog WHERE uid=? AND signDate LIKE ?"
async with cls.getDB().execute(sql, (uid, f"{monthStr}-%")) as cursor:
rows = await cursor.fetchall()
signedDays = {int(r[0][-2:]) for r in rows if r[0][-2:].isdigit()}
except Exception as e:
logger.warning("绘制签到图时数据库查询失败", e=e)
signedDays = set()
for day in range(1, totalDays + 1):
index = day + firstWeekday - 1
row = index // cols
col = index % cols
x1 = padding + col * cellSize
y1 = padding + titleHeight + row * cellSize
x2 = x1 + cellSize - 10
y2 = y1 + cellSize - 10
color = (112, 196, 112) if day in signedDays else (220, 220, 220)
await img.rectangle((x1, y1, x2, y2), fill=color, outline="black", width=2)
await img.text((x1 + 10, y1 + 10), str(day), font_size=24)
return img
@@ -0,0 +1,569 @@
import math
import re
from zhenxun.services.log import logger
from ...utils import config, getToolManager
from .database import CSqlManager
class CUserSoilDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userSoil"
async def initDB(self):
userSoil = {
"uid": "TEXT NOT NULL",
"soilIndex": "INTEGER NOT NULL", # 地块索引从1开始
"plantName": "TEXT DEFAULT ''", # 作物名称
"plantTime": "INTEGER DEFAULT 0", # 播种时间
"matureTime": "INTEGER DEFAULT 0", # 成熟时间
"soilLevel": "INTEGER DEFAULT 0", # 土地等级 0=普通地,1=红土地,2=黑土地,3=金土地
"wiltStatus": "INTEGER DEFAULT 0", # 枯萎状态 0=未枯萎,1=枯萎
"fertilizerStatus": "INTEGER DEFAULT 0", # 施肥状态 0=未施肥,1=施肥 2=增肥
"bugStatus": "INTEGER DEFAULT 0", # 虫害状态 0=无虫害,1=有虫害
"weedStatus": "INTEGER DEFAULT 0", # 杂草状态 0=无杂草,1=有杂草
"waterStatus": "INTEGER DEFAULT 0", # 缺水状态 0=不缺水,1=缺水
"harvestCount": "INTEGER DEFAULT 0", # 收获次数
"isSoilPlanted": "INTEGER DEFAULT NULL", # 是否种植作物 0=没有,1=有
"PRIMARY KEY": "(uid, soilIndex)",
}
await self.ensureTableSchema("userSoil", userSoil)
self.setInitialized()
@classmethod
async def nextPhase(cls, uid: str, soilIndex: int):
"""将指定地块的作物进入下个阶段
Args:
soilIndex (int): 地块索引 从1开始
"""
if not config.g_bIsDebug:
return
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return
plantInfo = await cls.getPlantManager().getPlantByName(soilInfo["plantName"])
if not plantInfo:
return
currentTime = getToolManager().dateTime().now().timestamp()
phaseList = await cls.getPlantManager().getPlantPhaseByName(
soilInfo["plantName"]
)
if currentTime >= soilInfo["matureTime"]:
return
elapsedTime = currentTime - soilInfo["plantTime"]
currentStage = currentStage = sum(1 for thr in phaseList if elapsedTime >= thr)
t = int(soilInfo["plantTime"]) - phaseList[currentStage]
s = int(soilInfo["matureTime"]) - phaseList[currentStage]
await cls.updateUserSoilFields(
uid, soilIndex, {"plantTime": t, "matureTime": s}
)
logger.debug(
f"当前阶段{currentStage}, 阶段时间{phaseList[currentStage]}, 播种时间{t}, 收获时间{s}"
)
@classmethod
async def matureNow(cls, uid: str, soilIndex: int):
"""将指定地块的作物直接成熟
Args:
uid (str): 用户ID
soilIndex (int): 地块索引(从1开始)
"""
# 与 nextPhase 不同:无需调试模式检查,允许在任何模式下调用
soilInfo = await cls.getUserSoil(uid, soilIndex)
if not soilInfo:
return
plantName = soilInfo.get("plantName")
if not plantName:
return
plantInfo = await cls.getPlantManager().getPlantByName(plantName)
if not plantInfo:
return
currentTime = int(getToolManager().dateTime().now().timestamp())
# 如果当前时间已经超过或等于成熟时间,则作物已成熟或可收获
if currentTime >= soilInfo["matureTime"]:
return
# 将作物成熟时间直接更新为当前时间,实现立即成熟
await cls.updateUserSoilFields(uid, soilIndex, {"matureTime": currentTime})
@classmethod
async def getUserFarmByUid(cls, uid: str) -> dict:
"""获取指定用户的旧农场数据
Args:
uid (str): 用户ID
Returns:
dict: 包含字段名-值的字典; 若无数据则返回空字典
"""
cursor = await cls.getDB().execute("SELECT * FROM soil WHERE uid = ?", (uid,))
row = await cursor.fetchone()
if not row:
return {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def insertUserSoil(cls, soilInfo: dict):
"""插入一条新的 userSoil 记录
Args:
soilInfo (dict): 新土地数据
Returns:
None
"""
async with cls._transaction():
await cls.getDB().execute(
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount, isSoilPlanted)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
soilInfo["soilIndex"],
soilInfo.get("plantName", ""),
soilInfo.get("plantTime", 0),
soilInfo.get("matureTime", 0),
soilInfo.get("soilLevel", 0),
soilInfo.get("wiltStatus", 0),
soilInfo.get("fertilizerStatus", 0),
soilInfo.get("bugStatus", 0),
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
soilInfo.get("isSoilPlanted", 0),
),
)
@classmethod
async def _insertUserSoil(cls, soilInfo: dict):
"""插入一条新的 userSoil 记录
Args:
soilInfo (dict): 新土地数据
Returns:
None
"""
await cls.getDB().execute(
"""
INSERT INTO userSoil
(uid, soilIndex, plantName, plantTime, matureTime,
soilLevel, wiltStatus, fertilizerStatus, bugStatus,
weedStatus, waterStatus, harvestCount, isSoilPlanted)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
soilInfo["uid"],
soilInfo["soilIndex"],
soilInfo.get("plantName", ""),
soilInfo.get("plantTime", 0),
soilInfo.get("matureTime", 0),
soilInfo.get("soilLevel", 0),
soilInfo.get("wiltStatus", 0),
soilInfo.get("fertilizerStatus", 0),
soilInfo.get("bugStatus", 0),
soilInfo.get("weedStatus", 0),
soilInfo.get("waterStatus", 0),
soilInfo.get("harvestCount", 0),
soilInfo.get("isSoilPlanted", 0),
),
)
@classmethod
async def getUserSoil(cls, uid: str, soilIndex: int) -> dict:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
dict: 记录存在返回字段-值字典,否则返回 None
"""
async with cls._transaction():
cursor = await cls.getDB().execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
(uid, soilIndex),
)
row = await cursor.fetchone()
if not row:
return {}
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def _getUserSoil(cls, uid: str, soilIndex: int) -> dict | None:
"""获取指定用户某块土地的详细信息
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
dict | None: 记录存在返回字段-值字典,否则返回 None
"""
cursor = await cls.getDB().execute(
"SELECT * FROM userSoil WHERE uid = ? AND soilIndex = ?",
(uid, soilIndex),
)
row = await cursor.fetchone()
if not row:
return None
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
@classmethod
async def countSoilByLevel(cls, uid: str, soilLevel: int) -> int:
"""统计指定用户在指定土地等级的土地数量
Args:
uid (str): 用户ID
soilLevel (int): 土地等级
Returns:
int: 符合条件的土地数量
"""
async with cls._transaction():
cursor = await cls.getDB().execute(
"SELECT COUNT(*) FROM userSoil WHERE uid = ? AND soilLevel = ?",
(uid, soilLevel),
)
row = await cursor.fetchone()
return row[0] if row else 0
@classmethod
async def updateUserSoil(cls, uid: str, soilIndex: int, field: str, value):
"""更新指定用户土地的单个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
field (str): 需更新的字段名
value: 新值
Returns:
None
"""
async with cls._transaction():
await cls.getDB().execute(
f"UPDATE userSoil SET {field} = ? WHERE uid = ? AND soilIndex = ?",
(value, uid, soilIndex),
)
@classmethod
async def _updateUserSoil(cls, uid: str, soilIndex: int, field: str, value):
"""更新指定用户土地的单个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
field (str): 需更新的字段名
value: 新值
Returns:
None
"""
await cls.getDB().execute(
f"UPDATE userSoil SET {field} = ? WHERE uid = ? AND soilIndex = ?",
(value, uid, soilIndex),
)
@classmethod
async def updateUserSoilFields(
cls, uid: str, soilIndex: int, updates: dict
) -> bool:
"""批量更新指定用户土地的多个字段
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
updates (dict): 字段-新值的字典
Returns:
bool: 如果无可更新字段则返回 False,否则更新成功返回 True
"""
# 允许更新的列白名单
allowedFields = {
"plantName",
"plantTime",
"matureTime",
"soilLevel",
"wiltStatus",
"fertilizerStatus",
"bugStatus",
"weedStatus",
"waterStatus",
"harvestCount",
"isSoilPlanted",
}
setClauses = []
values = []
for field, value in updates.items():
if field not in allowedFields:
continue
setClauses.append(f'"{field}" = ?')
values.append(value)
if not setClauses:
return False
values.extend([uid, soilIndex])
sql = f"UPDATE userSoil SET {', '.join(setClauses)} WHERE uid = ? AND soilIndex = ?"
try:
async with cls._transaction():
await cls.getDB().execute(sql, tuple(values))
return True
except Exception as e:
logger.error(f"批量更新土地字段失败: {e}")
return False
@classmethod
async def deleteUserSoil(cls, uid: str, soilIndex: int):
"""删除指定用户的土地记录
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
None
"""
async with cls._transaction():
await cls.getDB().execute(
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def _deleteUserSoil(cls, uid: str, soilIndex: int):
"""删除指定用户的土地记录
Args:
uid (str): 用户ID
soilIndex (int): 土地索引
Returns:
None
"""
await cls.getDB().execute(
"DELETE FROM userSoil WHERE uid = ? AND soilIndex = ?", (uid, soilIndex)
)
@classmethod
async def sowingByPlantName(cls, uid: str, soilIndex: int, plantName: str) -> bool:
"""播种指定作物到用户土地区
Args:
uid (str): 用户ID
soilIndex (int): 土地区索引
plantName (str): 植物名
Returns:
bool: 播种成功返回 True,否则返回 False
"""
# 校验土地区是否已种植
soilInfo = await cls.getUserSoil(uid, soilIndex)
if soilInfo and soilInfo.get("plantName"):
return False
# 获取植物配置""" """
plantCfg = await cls.getPlantManager().getPlantByName(plantName)
if not plantCfg:
logger.error(f"未知植物: {plantName}")
return False
nowTs = int(getToolManager().dateTime().now().timestamp())
time = int(plantCfg.get("time", 0))
percent = await cls.getSoilLevelTime(soilInfo.get("soilLevel", 0))
# 处理土地等级带来的时间缩短
time = math.floor(time * (100 + percent) // 100)
matureTs = nowTs + time * 3600
try:
async with cls._transaction():
prev = soilInfo or {}
await cls._deleteUserSoil(uid, soilIndex)
# 根据此植物收获次数对其星级进行升级
# 为避免对已带星级前缀的作物重复添加前缀,先提取基础名称
base_name = plantName
m = re.match(r"^(?:一星|二星|三星|四星|五星)", plantName)
if m:
base_name = plantName[len(m.group(0)) :]
# 只有当存在一星升级配置时才尝试按收获次数升级
if await cls.getPlantManager().existsPlant("一星" + base_name):
harvestCounts = await cls.getUserPlantCountManager().getUserPlantCountByPlantName(
uid, base_name
)
# 根据阈值设定最高星级(避免重复拼接)
prefix = ""
if harvestCounts >= 1200:
prefix = "三星"
elif harvestCounts >= 700:
prefix = "二星"
elif harvestCounts >= 350:
prefix = "一星"
plantName = prefix + base_name if prefix else base_name
await cls._insertUserSoil(
{
"uid": uid,
"soilIndex": soilIndex,
"plantName": plantName,
"plantTime": nowTs,
"matureTime": matureTs,
"soilLevel": prev.get("soilLevel", 0),
"wiltStatus": 0,
"fertilizerStatus": 0,
"bugStatus": 0,
"weedStatus": 0,
"waterStatus": 0,
"harvestCount": 0,
"isSoilPlanted": 1,
}
)
return True
except Exception as e:
logger.error("播种失败!", e=e)
return False
@classmethod
async def getUserSoilStatus(cls, uid: str, soilIndex: int) -> str:
status = []
soilInfo = await cls.getUserSoilManager().getUserSoil(uid, soilIndex)
if not soilInfo:
return ""
if soilInfo.get("wiltStatus", 0) == 1:
return "枯萎"
if soilInfo.get("fertilizerStatus", 0) == 1:
status.append("施肥")
elif soilInfo.get("fertilizerStatus", 0) == 2:
status.append("增肥")
if soilInfo.get("bugStatus", 0) == 1:
status.append("虫害")
if soilInfo.get("weedStatus", 0) == 1:
status.append("杂草")
if soilInfo.get("waterStatus", 0) == 1:
status.append("缺水")
return ",".join(status)
@classmethod
async def getSoilLevel(cls, level: int) -> str:
"""获取土地等级英文文本
Args:
level (int): 土地等级
Returns:
str:
"""
if level == 1:
return "red"
elif level == 2:
return "black"
elif level == 3:
return "gold"
return "default"
@classmethod
async def getSoilLevelText(cls, level: int) -> str:
"""获取土地等级中文文本
Args:
level (int): 土地等级
Returns:
str:
"""
if level == 1:
return "红土地"
elif level == 2:
return "黑土地"
elif level == 3:
return "金土地"
return "草土地"
@classmethod
async def getSoilLevelHarvestNumber(cls, level: int) -> int:
"""获取土地等级收获数量增加比例
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 2:
return 20
elif level == 3:
return 28
return 10
@classmethod
async def getSoilLevelHarvestExp(cls, level: int) -> int:
"""获取土地等级收获经验增加比例
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 3:
return 28
return 0
@classmethod
async def getSoilLevelTime(cls, level: int) -> int:
"""获取土地等级播种减少时间消耗
Args:
level (int): 土地等级
Returns:
int:
"""
if level == 2:
return 20
elif level == 3:
return 20
return 0
@@ -0,0 +1,226 @@
from zhenxun.services.log import logger
from .database import CSqlManager
class CUserStealDB(CSqlManager):
def __init__(self):
super().__init__()
self.m_sTableName = "userSteal"
async def initDB(self):
userSteal = {
"uid": "TEXT NOT NULL", # 被偷用户Uid
"soilIndex": "INTEGER NOT NULL", # 被偷的地块索引 从1开始
"stealerUid": "TEXT NOT NULL", # 偷菜用户Uid
"stealCount": "INTEGER NOT NULL", # 被偷数量
"stealTime": "INTEGER NOT NULL", # 被偷时间
"PRIMARY KEY": "(uid, soilIndex, stealerUid)",
}
await self.ensureTableSchema("userSteal", userSteal)
self.setInitialized()
@classmethod
async def addStealRecord(
cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int
) -> bool:
"""添加偷菜记录
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
stealCount (int): 被偷数量
stealTime (int): 被偷时间(时间戳)
Returns:
bool: 操作是否成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
'INSERT INTO "userSteal"(uid, soilIndex, stealerUid, stealCount, stealTime) VALUES(?, ?, ?, ?, ?);',
(uid, soilIndex, stealerUid, stealCount, stealTime),
)
return True
except Exception as e:
logger.warning("添加偷菜记录失败", e=e)
return False
@classmethod
async def getStealRecordsByUid(cls, uid: str) -> dict:
"""根据用户Uid获取所有偷菜记录
Args:
uid (str): 被偷用户Uid
Returns:
list: 偷菜记录字典列表,每条包含 soilIndex, stealerUid, stealCount, stealTime
"""
try:
async with cls._transaction():
cursor = await cls.getDB().execute(
'SELECT soilIndex, stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=?;',
(uid,),
)
row = await cursor.fetchone()
if not row:
return {}
result = dict(row)
return result
except Exception as e:
logger.warning("获取偷菜记录失败", e=e)
return {}
@classmethod
async def getStealRecord(cls, uid: str, soilIndex: int) -> list:
"""获取指定地块的所有偷菜记录
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
list: 偷菜记录字典列表,每条包含 stealerUid, stealCount, stealTime
"""
try:
async with cls._transaction():
cursor = await cls.getDB().execute(
'SELECT stealerUid, stealCount, stealTime FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
rows = await cursor.fetchall()
return [
{
"uid": uid,
"soilIndex": soilIndex,
"stealerUid": row[0],
"stealCount": row[1],
"stealTime": row[2],
}
for row in rows
]
except Exception as e:
logger.warning("获取单地块偷菜记录失败", e=e)
return []
@classmethod
async def getTotalStolenCount(cls, uid: str, soilIndex: int) -> int:
"""计算指定地块被偷的总数量(所有用户偷取数量之和)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
int: 被偷的总数量,如果无记录则返回 0
"""
try:
async with cls._transaction():
cursor = await cls.getDB().execute(
'SELECT SUM(stealCount) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算总偷菜数量失败", e=e)
return 0
@classmethod
async def getStealerCount(cls, uid: str, soilIndex: int) -> int:
"""计算指定地块被多少人偷过(不同偷菜用户数量)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
int: 偷菜者总数,如果无记录则返回 0
"""
try:
async with cls._transaction():
cursor = await cls.getDB().execute(
'SELECT COUNT(DISTINCT stealerUid) FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
row = await cursor.fetchone()
return row[0] or 0 # type: ignore
except Exception as e:
logger.warning("计算偷菜者数量失败", e=e)
return 0
@classmethod
async def hasStealed(cls, uid: str, soilIndex: int, stealerUid: str) -> bool:
"""判断指定用户是否曾偷取过该地块
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
Returns:
bool: 若存在记录返回 True,否则返回 False
"""
try:
async with cls._transaction():
cursor = await cls.getDB().execute(
'SELECT 1 FROM "userSteal" WHERE uid=? AND soilIndex=? AND stealerUid=? LIMIT 1;',
(uid, soilIndex, stealerUid),
)
row = await cursor.fetchone()
return bool(row)
except Exception as e:
logger.warning("检查偷菜记录失败", e=e)
return False
@classmethod
async def updateStealRecord(
cls, uid: str, soilIndex: int, stealerUid: str, stealCount: int, stealTime: int
) -> bool:
"""更新偷菜记录的数量和时间
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
stealerUid (str): 偷菜用户Uid
stealCount (int): 新的偷菜数量
stealTime (int): 新的偷菜时间(时间戳)
Returns:
bool: 操作是否成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
'UPDATE "userSteal" SET stealCount=?, stealTime=? WHERE uid=? AND soilIndex=? AND stealerUid=?;',
(stealCount, stealTime, uid, soilIndex, stealerUid),
)
return True
except Exception as e:
logger.warning("更新偷菜记录失败", e=e)
return False
@classmethod
async def deleteStealRecord(cls, uid: str, soilIndex: int) -> bool:
"""删除指定偷菜记录(只需被偷用户Uid和地块索引)
Args:
uid (str): 被偷用户Uid
soilIndex (int): 被偷地块索引
Returns:
bool: 删除是否成功
"""
try:
async with cls._transaction():
await cls.getDB().execute(
'DELETE FROM "userSteal" WHERE uid=? AND soilIndex=?;',
(uid, soilIndex),
)
return True
except Exception as e:
logger.warning("删除偷菜记录失败", e=e)
return False
@@ -0,0 +1,115 @@
import asyncio
from typing import Any
from zhenxun.services.log import logger
from .database import (
CPlantManager,
CUserDB,
CUserItemDB,
CUserPlantCountDB,
CUserPlantDB,
CUserSeedDB,
CUserSignDB,
CUserSoilDB,
CUserStealDB,
g_pDBlocator,
)
class CDBService:
"""
数据库服务管理器
负责初始化和协调所有数据库服务
"""
def __init__(self):
self.m_manager: dict[str, Any] = {}
self.m_bInitialized = False
async def init(self):
if self.m_bInitialized:
logger.warning("真寻农场数据库服务已初始化")
return
m_manager = {
"plant": CPlantManager(),
"user": CUserDB(),
"userSoil": CUserSoilDB(),
"userPlant": CUserPlantDB(),
"userSeed": CUserSeedDB(),
"userItem": CUserItemDB(),
"userSteal": CUserStealDB(),
"userSign": CUserSignDB(),
"userPlantCount": CUserPlantCountDB(),
}
for name, manager in m_manager.items():
g_pDBlocator.register(name, manager)
self.m_manager[name] = manager
tasks = []
for name, manager in self.m_manager.items():
tasks.append(manager.init())
tasks.append(manager.initDB())
await asyncio.gather(*tasks)
g_pDBlocator.markInitialized()
self.m_bInitialized = True
logger.debug("真寻农场数据库服务初始化完成")
async def cleanup(self):
task = []
for manager in self.m_manager.values():
if hasattr(manager, "cleanup"):
task.append(manager.cleanup())
await asyncio.gather(*task)
self.m_manager.clear()
self.m_bInitialized = False
@property
def plant(self):
return self.m_manager["plant"]
@property
def user(self):
return self.m_manager["user"]
@property
def userSoil(self):
return self.m_manager["userSoil"]
@property
def userPlant(self):
return self.m_manager["userPlant"]
@property
def userSeed(self):
return self.m_manager["userSeed"]
@property
def userItem(self):
return self.m_manager["userItem"]
@property
def userSteal(self):
return self.m_manager["userSteal"]
@property
def userSign(self):
return self.m_manager["userSign"]
@property
def userPlantCount(self):
return self.m_manager["userPlantCount"]
@property
def initialized(self) -> bool:
return self.m_bInitialized
g_pDBService = CDBService()
+891
View File
@@ -0,0 +1,891 @@
import math
import random
import re
from zhenxun.configs.config import Config
from zhenxun.models.user_console import UserConsole
from zhenxun.services.log import logger
from zhenxun.utils.enum import GoldHandle
from zhenxun.utils.image_utils import ImageTemplate
from ..core.dbService import g_pDBService
from ..event import g_pEventManager
from ..utils import config, getJsonManager, getToolManager
class CFarmManager:
@classmethod
async def buyPointByUid(cls, uid: str, num: int) -> str:
if num <= 0:
return "你是怎么做到购买不是正数的农场币的"
user = await UserConsole.get_user(uid)
pro = float(Config.get_config("zhenxun_plugin_farm", "兑换倍数"))
tax = float(Config.get_config("zhenxun_plugin_farm", "手续费"))
# 计算手续费
fee = math.floor(num * tax)
# 实际扣费金额
deduction = num + fee
if user.gold < deduction:
return f"你的金币不足或不足承担手续费。当前手续费为{fee}"
await UserConsole.reduce_gold(
uid,
num,
GoldHandle.PLUGIN, # type: ignore
"zhenxun_plugin_farm",
)
await UserConsole.reduce_gold(
uid,
fee,
GoldHandle.PLUGIN, # type: ignore
"zhenxun_plugin_farm",
)
point = num * pro
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
await player.addPoint("point", int(point))
return f"充值{point}农场币成功,手续费{tax}金币,当前农场币:{player.user.get('point', 0)}"
@classmethod
async def getUserSeedByUid(cls, uid: str) -> bytes:
"""获取用户种子仓库"""
dataList = []
columnNames = [
"-",
"种子名称",
"数量",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"是否可以上架交易行",
]
# 从数据库获取结构化数据
seedRecords = await g_pDBService.userSeed.getUserSeedByUid(uid) or {}
if not seedRecords:
result = await ImageTemplate.table_page(
"种子仓库",
"播种示例:@小真寻 播种 大白菜 [数量]",
columnNames,
dataList,
)
return result.pic2bytes()
for seedName, count in seedRecords.items():
try:
plantInfo = await g_pDBService.plant.getPlantByName(seedName)
if not plantInfo:
continue
iconPath = config.g_sResourcePath / f"plant/{seedName}/icon.png"
icon = (iconPath, 33, 33) if iconPath.exists() else ""
sellable = "可以" if plantInfo["sell"] else "不可以"
dataList.append(
[
icon,
seedName,
count,
plantInfo["experience"],
plantInfo["harvest"],
plantInfo["time"],
plantInfo["crop"],
sellable,
]
)
except KeyError:
continue
result = await ImageTemplate.table_page(
"种子仓库",
"播种示例:@小真寻 播种 大白菜 [数量]",
columnNames,
dataList,
)
return result.pic2bytes()
@classmethod
async def sowing(cls, uid: str, name: str, num: int = -1) -> str:
"""播种
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int, optional): 播种数量
Returns:
str: 返回结果
"""
try:
# 获取用户的种子数量
count = await g_pDBService.userSeed.getUserSeedByName(uid, name)
if count is None:
count = 0 # 如果返回 None,则视为没有种子
if count <= 0:
return config.g_sTranslation["sowing"]["noSeed"].format(name=name)
# 如果播种数量超过仓库种子数量
if count < num and num != -1:
return config.g_sTranslation["sowing"]["noNum"].format(
name=name, num=count
)
# 获取用户土地数量
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
soilNumber = player.user.get("soil", 3)
# 如果播种数量为 -1,表示播种所有可播种的土地
if num == -1:
num = count
# 发送播种前信号
await g_pEventManager.m_beforePlant.emit(uid=uid, name=name, num=num) # type: ignore
# 记录是否成功播种
successCount = 0
for i in range(1, soilNumber + 1):
if count > 0 and num > 0:
success = await g_pDBService.userSoil.sowingByPlantName(
uid, i, name
)
if success:
# 更新种子数量
num -= 1
count -= 1
# 记录种子消耗数量
successCount += 1
# 发送播种后信号
await g_pEventManager.m_afterPlant.emit( # type: ignore
uid=uid, name=name, soilIndex=i
)
# 确保用户仓库数量更新
if successCount > 0:
await g_pDBService.userSeed.updateUserSeedByName(uid, name, count)
# 根据播种结果给出反馈
if num == 0:
return config.g_sTranslation["sowing"]["success"].format(
name=name, num=count
)
else:
return config.g_sTranslation["sowing"]["success2"].format(
name=name, num=count
)
except Exception as e:
logger.warning("播种操作失败!", e=e)
return config.g_sTranslation["sowing"]["error"]
@classmethod
async def harvest(cls, uid: str) -> str:
"""收获作物
Args:
uid (str): 用户Uid
Returns:
str: 返回
"""
try:
await g_pEventManager.m_beforeHarvest.emit(uid=uid) # type: ignore
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
soilNumber = player.user.get("soil", 3)
harvestRecords = [] # 收获日志记录
experience = 0 # 总经验值
harvestCount = 0 # 成功收获数量
for i in range(1, soilNumber + 1):
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, i)
if not soilInfo:
continue
# 如果没有种植
if soilInfo.get("isSoilPlanted", 1) == 0:
continue
level = soilInfo.get("soilLevel", 0)
# 如果是枯萎状态
if soilInfo.get("wiltStatus", 1) == 1:
continue
plantInfo = await g_pDBService.plant.getPlantByName(
soilInfo["plantName"]
)
if not plantInfo:
continue
currentTime = getToolManager().dateTime().now()
matureTime = (
getToolManager()
.dateTime()
.fromtimestamp(int(soilInfo["matureTime"]))
)
if currentTime >= matureTime:
number = plantInfo["harvest"]
# 处理偷菜扣除数量
stealNum = await g_pDBService.userSteal.getTotalStolenCount(uid, i)
number -= stealNum
# 处理土地等级带来的数量增长 向下取整
percent = await g_pDBService.userSoil.getSoilLevelHarvestNumber(
level
)
number = math.floor(number * (100 + percent) // 100)
if number <= 0:
continue
harvestCount += 1
experience += plantInfo["experience"]
# 处理土地等级带来的经验增长 向下取整
percent = await g_pDBService.userSoil.getSoilLevelHarvestExp(level)
experience = math.floor(experience * (100 + percent) // 100)
harvestRecords.append(
config.g_sTranslation["harvest"]["append"].format(
name=soilInfo["plantName"],
num=number,
exp=plantInfo["experience"],
)
)
await g_pDBService.userPlant.addUserPlantByUid(
uid, soilInfo["plantName"], number
)
# 统计收获次数
try:
# 处理星级前缀(只在前两字为 一星/二星/三星/四星/五星 时去掉前缀)
plant_name = soilInfo["plantName"]
m = re.match(r"^(?:一星|二星|三星|四星|五星)", plant_name)
if m:
plant_name = plant_name[len(m.group(0)) :]
await g_pDBService.userPlantCount.addUserPlantCountByUid(
uid, plant_name
)
except Exception:
# 忽略统计异常,避免影响主流程
pass
# 如果到达收获次数上限
if soilInfo["harvestCount"] + 1 >= plantInfo["crop"]:
await g_pDBService.userSoil.updateUserSoil(
uid, i, "wiltStatus", 1
)
else:
phase = await g_pDBService.plant.getPlantPhaseByName(
soilInfo["plantName"]
)
ts, hc = (
int(currentTime.timestamp()),
soilInfo["harvestCount"] + 1,
)
p1, p2, *rest = phase
await g_pDBService.userSoil.updateUserSoilFields(
uid,
i,
{
"harvestCount": hc,
"plantTime": ts - p1 - p2,
"matureTime": ts + p2 + sum(rest),
},
)
await g_pEventManager.m_afterHarvest.emit( # type: ignore
uid=uid, name=soilInfo["plantName"], num=number, soilIndex=i
)
if experience > 0:
exp = player.user.get("exp", 0)
await player.addExp(exp + experience)
harvestRecords.append(
config.g_sTranslation["harvest"]["exp"].format(
exp=experience,
)
)
if harvestCount <= 0:
return config.g_sTranslation["harvest"]["no"]
else:
return "\n".join(harvestRecords)
except Exception as e:
logger.warning("收获操作失败!", e=e)
return config.g_sTranslation["harvest"]["error"]
@classmethod
async def eradicate(cls, uid: str) -> str:
"""铲除作物
TODO 缺少随意铲除作物 目前只能铲除荒废作物
Args:
uid (str): 用户Uid
Returns:
str: 返回
"""
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
soilNumber = player.user.get("soil", 3)
await g_pEventManager.m_beforeEradicate.emit(uid=uid) # type: ignore
experience = 0
for i in range(1, soilNumber + 1):
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, i)
if not soilInfo:
continue
# 如果没有种植
if soilInfo.get("isSoilPlanted", 1) == 0:
continue
# 如果不是枯萎状态
if soilInfo.get("wiltStatus", 0) == 0:
continue
experience += 3
if config.g_bIsDebug:
experience += 999
# 更新数据库操作
await g_pDBService.userSoil.deleteUserSoil(uid, i)
await g_pDBService.userSoil.updateUserSoilFields(
uid,
i,
{
"plantName": "",
"plantTime": 0,
"matureTime": 0,
"wiltStatus": 0,
"isSoilPlanted": 0,
},
)
# 铲除作物会将偷菜记录清空
await g_pDBService.userSteal.deleteStealRecord(uid, i)
await g_pEventManager.m_afterEradicate.emit(uid=uid, soilIndex=i) # type: ignore
if experience > 0:
exp = player.user.get("exp", 0)
await player.addExp(exp + experience)
return config.g_sTranslation["eradicate"]["success"].format(exp=experience)
else:
return config.g_sTranslation["eradicate"]["error"]
@classmethod
async def getUserPlantByUid(cls, uid: str) -> bytes:
"""获取用户作物仓库
Args:
uid (str): 用户Uid
Returns:
bytes: 返回图片
"""
data_list = []
column_name = [
"-",
"作物名称",
"数量",
"单价",
"总价",
"是否上锁",
"是否可以上架交易行",
]
plant = await g_pDBService.userPlant.getUserPlantByUid(uid)
if plant is None:
result = await ImageTemplate.table_page(
"作物仓库",
"出售示例:@小真寻 出售作物 大白菜 [数量]",
column_name,
data_list,
)
return result.pic2bytes()
sell = ""
for name, count in plant.items():
plantInfo = await g_pDBService.plant.getPlantByName(name)
if not plantInfo:
continue
icon = ""
icon_path = config.g_sResourcePath / f"plant/{name}/icon.png"
if icon_path.exists():
icon = (icon_path, 33, 33)
if plantInfo["sell"]:
sell = "可以"
else:
sell = "不可以"
number = int(count) * plantInfo["price"]
isLock = await g_pDBService.userPlant.checkPlantLockByName(uid, name)
if isLock:
lock = "上锁"
else:
lock = "未上锁"
data_list.append(
[icon, name, count, plantInfo["price"], number, lock, sell]
)
result = await ImageTemplate.table_page(
"作物仓库",
"出售示例:@小真寻 出售作物 大白菜 [数量]",
column_name,
data_list,
)
return result.pic2bytes()
@classmethod
async def lockUserPlantByUid(cls, uid: str, name: str, lock: int) -> str:
"""加/解 锁用户作物
Args:
uid (str): 用户uid
name (str): 作物名称
lock (int): 0=解锁 非0=加锁
Returns:
str: 返回
"""
# 先判断该用户仓库是否有该作物
if not await g_pDBService.userPlant.checkUserPlantByName(uid, name):
return config.g_sTranslation["lockPlant"]["noPlant"]
# 更改加锁状态
if await g_pDBService.userPlant.lockUserPlantByName(uid, name, lock):
if lock == 0:
return config.g_sTranslation["lockPlant"]["unlockPlant"].format(
name=name
)
else:
return config.g_sTranslation["lockPlant"]["lockPlant"].format(name=name)
else:
return config.g_sTranslation["lockPlant"]["error"]
@classmethod
async def stealing(cls, uid: str, target: str) -> str:
"""偷菜
Args:
uid (str): 用户Uid
target (str): 被偷用户Uid
Returns:
str: 返回
"""
# 用户信息
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
stealTime = player.user.get("stealTime", "")
stealCount = int(player.user["stealCount"])
if stealTime == "" or not stealTime:
stealTime = getToolManager().dateTime().date().today().strftime("%Y-%m-%d")
stealCount = 5
elif (
getToolManager().dateTime().date().fromisoformat(stealTime)
!= getToolManager().dateTime().date().today()
):
stealTime = getToolManager().dateTime().date().today().strftime("%Y-%m-%d")
stealCount = 5
if stealCount <= 0:
return config.g_sTranslation["stealing"]["max"]
# 获取用户解锁地块数量
soilNumber = player.user.get("soil", 3)
harvestRecords: list[str] = []
isStealingNumber = 0
isStealingPlant = 0
for i in range(1, soilNumber + 1):
soilInfo = await g_pDBService.userSoil.getUserSoil(target, i)
if not soilInfo:
continue
# 如果没有种植
if soilInfo.get("isSoilPlanted", 1) == 0:
continue
# 如果是枯萎状态
if soilInfo.get("wiltStatus", 1) == 1:
continue
# 作物信息
plantInfo = await g_pDBService.plant.getPlantByName(soilInfo["plantName"])
if not plantInfo:
continue
currentTime = getToolManager().dateTime().now()
matureTime = (
getToolManager().dateTime().fromtimestamp(int(soilInfo["matureTime"]))
)
if currentTime >= matureTime:
# 如果偷过,则跳过该土地
if await g_pDBService.userSteal.hasStealed(target, i, uid):
isStealingNumber += 1
continue
stealingNumber = plantInfo[
"harvest"
] - await g_pDBService.userSteal.getTotalStolenCount(target, i)
randomNumber = random.choice([1, 2])
randomNumber = min(randomNumber, stealingNumber)
if randomNumber > 0:
await g_pDBService.userPlant.addUserPlantByUid(
uid, soilInfo["plantName"], randomNumber
)
harvestRecords.append(
config.g_sTranslation["stealing"]["info"].format(
name=soilInfo["plantName"], num=randomNumber
)
)
isStealingPlant += 1
# 如果将作物偷完,就直接更新状态 并记录用户偷取过
if plantInfo["harvest"] - randomNumber + stealingNumber == 0:
# 如果作物 是最后一阶段作物且偷完 则直接枯萎
if soilInfo["harvestCount"] + 1 >= plantInfo["crop"]:
await g_pDBService.userSoil.updateUserSoil(
target, i, "wiltStatus", 1
)
else:
phase = await g_pDBService.plant.getPlantPhaseByName(
soilInfo["plantName"]
)
ts, hc = (
int(currentTime.timestamp()),
soilInfo["harvestCount"] + 1,
)
p1, p2, *rest = phase
await g_pDBService.userSoil.updateUserSoilFields(
uid,
i,
{
"harvestCount": hc,
"plantTime": ts - p1 - p2,
"matureTime": ts + p2 + sum(rest),
},
)
await g_pDBService.userSteal.addStealRecord(
target,
i,
uid,
randomNumber,
int(getToolManager().dateTime().now().timestamp()),
)
else:
await g_pDBService.userSteal.addStealRecord(
target,
i,
uid,
randomNumber,
int(getToolManager().dateTime().now().timestamp()),
)
if isStealingPlant <= 0 and isStealingNumber <= 0:
return config.g_sTranslation["stealing"]["noPlant"]
elif isStealingPlant <= 0 and isStealingNumber > 0:
return config.g_sTranslation["stealing"]["repeat"]
else:
stealCount -= 1
await player.updateStealCountByUid(uid, stealTime, stealCount)
return "\n".join(harvestRecords)
@classmethod
async def reclamationCondition(cls, uid: str) -> str:
"""获取开垦条件
Args:
uid (str): 用户Uid
Returns:
str: 返回条件文本信息
"""
jLevel = await getJsonManager().getLevel()
rec = jLevel["reclamation"]
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
try:
if player.user["soil"] >= 30:
return config.g_sTranslation["reclamation"]["perfect"]
rec = rec[f"{player.user['soil'] + 1}"]
level = rec["level"]
point = rec["point"]
item = rec["item"]
str = ""
if len(item) == 0:
str = config.g_sTranslation["reclamation"]["next"].format(
level=level, num=point
)
else:
str = config.g_sTranslation["reclamation"]["next2"].format(
level=level, num=point, item=item
)
return str
except Exception:
return config.g_sTranslation["reclamation"]["error"]
@classmethod
async def reclamation(cls, uid: str) -> str:
"""开垦
Args:
uid (str): 用户Uid
Returns:
str: _description_
"""
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
level = await player.getUserLevel()
jsonLevel = await getJsonManager().getLevel()
rec = jsonLevel["reclamation"]
try:
if player.user["soil"] >= 30:
return config.g_sTranslation["reclamation"]["perfect"]
rec = rec[f"{player.user['soil'] + 1}"]
levelFileter = rec["level"]
point = rec["point"]
# item = rec["item"]
if level[0] < levelFileter:
return config.g_sTranslation["reclamation"]["nextLevel"].format(
level=level[0], next=levelFileter
)
if player.user["point"] < point:
return config.g_sTranslation["reclamation"]["noNum"].format(num=point)
# TODO 缺少判断消耗的item
await player.subPoint("point", point)
await player.updateField("soil", player.user["soil"] + 1)
return config.g_sTranslation["reclamation"]["success"]
except Exception:
return config.g_sTranslation["reclamation"]["error1"]
@classmethod
async def soilUpgradeCondition(cls, uid: str, soilIndex: int) -> str:
"""获取土地升级条件
Args:
uid (str): 用户Uid
soilIndex (str): 土地索引
Returns:
str: 返回土地升级条件
"""
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, soilIndex)
if not soilInfo:
return config.g_sTranslation["soilInfo"]["error"]
soilLevel = soilInfo.get("soilLevel", 0) + 1
if soilLevel >= config.g_iSoilLevelMax:
return config.g_sTranslation["soilInfo"]["error1"]
# 获取用户当前土地 的下一级土地 数量
countSoil = await g_pDBService.userSoil.countSoilByLevel(uid, soilLevel)
# 获取升级所需
soilLevelText = await g_pDBService.userSoil.getSoilLevel(soilLevel)
jSoil = await getJsonManager().getSoil()
fileter = jSoil["upgrade"][soilLevelText][countSoil]
nextLevel = await g_pDBService.userSoil.getSoilLevelText(soilLevel)
lines = ["将土地升级至:" + nextLevel + "。", "所需:"]
fields = [
("level", "等级"),
("point", "金币"),
("vipPoint", "点券"),
]
for key, label in fields:
value = fileter.get(key, 0)
if value > 0:
lines.append(f"{label}:{value}")
items = fileter.get("item", {})
for name, qty in items.items():
if qty:
lines.append(f"{name}:{qty}")
lines.append("回复“是”将执行升级")
return "\n".join(lines)
@classmethod
async def soilUpgrade(cls, uid: str, soilIndex: int) -> str:
"""土地升级
Args:
uid (str): 用户Uid
soilIndex (int): 土地索引
Returns:
str:
"""
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
soilInfo = await g_pDBService.userSoil.getUserSoil(uid, soilIndex)
if not soilInfo:
return config.g_sTranslation["soilInfo"]["error"]
soilLevel = soilInfo.get("soilLevel", 0) + 1
if soilLevel >= config.g_iSoilLevelMax:
return config.g_sTranslation["soilInfo"]["error1"]
countSoil = await g_pDBService.userSoil.countSoilByLevel(uid, soilLevel)
soilLevelText = await g_pDBService.userSoil.getSoilLevel(soilLevel)
soil = await getJsonManager().getSoil()
fileter = soil["upgrade"][soilLevelText][countSoil]
getters = {
"level": (await player.getUserLevel())[0],
"point": player.user.get("point", 0),
"vipPoint": player.user.get("vipPoint", 0),
}
requirements = {
"level": "等级",
"point": "金币",
"vipPoint": "点券",
}
for key, val in getters.items():
need = fileter.get(key, 0)
if val < need:
return f"你的{requirements[key]}不够哦~"
# 缺少item判断
# 更新数据库字段
await g_pDBService.userSoil.updateUserSoil(
uid, soilIndex, "soilLevel", soilLevel
)
# 如果有作物的话直接成熟
await g_pDBService.userSoil.matureNow(uid, soilIndex)
# 更新数据库字段
await player.subPoint("point", fileter.get("point", 0))
await player.subPoint("vipPoint", fileter.get("vipPoint", 0))
return config.g_sTranslation["soilInfo"]["success"].format(
name=await g_pDBService.userSoil.getSoilLevelText(soilLevel),
text=config.g_sTranslation["soilInfo"][soilLevelText],
)
@classmethod
async def pointToVipPointByUid(cls, uid: str, num: int) -> str:
"""点券兑换
Args:
uid (str): 用户Uid
num (int): 兑换点券数量
Returns:
str: 返回结果
兑换比例在配置文件中配置
目前配置文件中默认是20倍
100点券需要20000农场币
赠送点券规则:
小于2000点券:0
2000-5000点券:100
5000-50000点券:280
大于50000点券:3000
"""
if num < 100:
return "点券兑换数量必须大于等于100"
pro = int(Config.get_config("zhenxun_plugin_farm", "点券兑换倍数"))
pro *= num
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
point = player.user.get("point", 0)
if point < pro:
return f"你的农场币不足,当前农场币为{point},兑换还需要{pro - point}农场币"
giftPoints: int
if num < 2000:
giftPoints = 0
elif num < 5000:
giftPoints = 100
elif num < 50000:
giftPoints = 280
else:
giftPoints = 3000
number = num + giftPoints
await player.addPoint("vipPoint", number)
await player.subPoint("point", pro)
return f"兑换{num}点券成功,当前点券:{number},赠送点券:{giftPoints},当前农场币:{point}"
+35
View File
@@ -0,0 +1,35 @@
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.log import logger
from ..utils import config, getToolManager
class CHelpManager:
@classmethod
async def createHelpImage(cls) -> bool:
templatePath = config.g_sResourcePath / "html/help.html"
outputPath = config.g_sResourcePath / "temp_html/help.html"
savePath = DATA_PATH / "farm_res/html/help.png"
context = {
"main_title": "真寻农场帮助菜单",
"subtitle": "[]中为可选参数",
"page_title": "真寻农场帮助菜单",
"font_family": "MyFont",
"contents": [
{"title": "主要指令", "commands": ["指令A", "指令B"]},
{"title": "B", "commands": ["指令D", "指令E", "指令M", "指令i"]},
],
}
try:
getToolManager().renderHtmlToFile(templatePath, context, outputPath)
bytes = await getToolManager().screenshotSave(
str(outputPath), str(savePath), 1500, 2300
)
except Exception as e:
logger.warning("绘制农场帮助菜单失败", e=e)
return False
return True
@@ -0,0 +1,4 @@
from .player import CPlayer
from .playerPool import CPlayerPool
__all__ = ["CPlayer", "CPlayerPool"]
@@ -0,0 +1,221 @@
from .. import g_pDBlocator
class CPlayer:
def __init__(self):
self.user = {
"uid": "", # 用户Uid
"name": "", # 农场名称
"exp": 0, # 经验值
"point": 0, # 金币
"vipPoint": 0, # 点券
"soil": 3, # 解锁土地数量
"stealTime": "", # 偷菜时间字符串
"stealCount": 0, # 剩余偷菜次数
}
async def init(self, uid: str) -> bool:
self.user["uid"] = uid
return await self.loadFormDB()
async def loadFormDB(self) -> bool:
uid = self.user.get("uid", "")
if uid == "":
return False
self.user = await g_pDBlocator.getUserManager().getUserInfoByUid(uid)
return True
async def isRegistered(self) -> bool:
"""检查用户是否注册农场
Returns:
bool: 是否注册农场
"""
uid = self.user.get("uid", "")
if uid == "":
return False
return await g_pDBlocator.getUserManager().isRegistered(uid)
async def addPoint(self, type: str, index: int) -> bool:
"""增加货币
Args:
type (str): 货币类型 point/vipPoint
index (int): 增加的数量
Returns:
bool: 是否成功增加货币
"""
uid = self.user.get("uid", "")
if uid == "" or type not in g_pDBlocator.getUserManager().currencies:
return False
if index == 0:
return True
nowIndex = self.user.get(type, 0) + index
if nowIndex < 0:
nowIndex = 0
if await g_pDBlocator.getUserManager().updatePoint(uid, type, nowIndex):
self.user[type] = nowIndex
return True
return False
async def subPoint(self, type: str, index: int) -> bool:
"""减少货币
Args:
type (str): 货币类型 point/vipPoint
index (int): 减少的数量
Returns:
bool: 是否成功减少货币
"""
uid = self.user.get("uid", "")
if uid == "" or type not in g_pDBlocator.getUserManager().currencies:
return False
if index == 0:
return True
nowIndex = self.user.get(type, 0) - index
if nowIndex < 0:
nowIndex = 0
if await g_pDBlocator.getUserManager().updatePoint(uid, type, nowIndex):
self.user[type] = nowIndex
return True
return False
async def addExp(self, exp: int) -> bool:
"""增加经验值
Args:
exp (int): 增加的经验值
Returns:
bool: 是否成功增加经验值
"""
uid = self.user.get("uid", "")
if uid == "":
return False
if exp == 0:
return True
nowExp = self.user.get("exp", 0) + exp
if nowExp < 0:
nowExp = 0
if await g_pDBlocator.getUserManager().updateExp(uid, nowExp):
self.user["exp"] = nowExp
return True
return False
async def subExp(self, exp: int) -> bool:
"""减少经验值
Args:
exp (int): 减少的经验值
Returns:
bool: 是否成功减少经验值
"""
uid = self.user.get("uid", "")
if uid == "":
return False
if exp == 0:
return True
nowExp = self.user.get("exp", 0) - exp
if nowExp < 0:
nowExp = 0
if await g_pDBlocator.getUserManager().updateExp(uid, nowExp):
self.user["exp"] = nowExp
return True
return False
async def updateName(self, name: str) -> str:
"""更新农场名称
Args:
name (str): 农场名称
Returns:
str: success/error/error1
"""
uid = self.user.get("uid", "")
if uid == "":
return "error"
return await g_pDBlocator.getUserManager().updateName(uid, name)
async def getUserLevel(self) -> tuple[int, int, int]:
"""获取用户等级信息
Returns:
tuple[int, int, int]: 成功返回(当前等级, 升至下级还需经验, 当前等级已获经验)
失败返回(-1, -1, -1)
"""
uid = self.user.get("uid", "")
if uid == "":
return -1, -1, -1
return await g_pDBlocator.getUserManager().getUserLevelByUid(uid)
async def updateStealCountByUid(
self, uid: str, stealTime: str, stealCount: int
) -> bool:
"""根据用户Uid更新剩余偷菜次数
Args:
uid (str): 用户Uid
stealTime (str): 偷菜日期
stealCount (int): 新剩余偷菜次数
Returns:
bool: 是否更新成功
"""
uid = self.user.get("uid", "")
if uid == "":
return False
return await g_pDBlocator.getUserManager().updateStealCountByUid(
uid, stealTime, stealCount
)
async def updateField(self, field: str, value) -> bool:
"""更新单字段信息
Returns:
bool: 是否成功更新单字段信息
"""
uid = self.user.get("uid", "")
if uid == "":
return False
return await g_pDBlocator.getUserManager().updateFieldByUid(uid, field, value)
@@ -0,0 +1,212 @@
import threading
import time
from typing import Any
from zhenxun.services.log import logger
class CPlayerPool:
"""
用户池管理类
管理用户对象的生命周期,支持自动清理超时用户
"""
def __init__(self, timeoutSeconds: int = 300, cleanupInterval: int = 3600):
"""
初始化用户池
Args:
timeoutSeconds: 用户超时时间(秒),默认5分钟
cleanupInterval: 清理间隔(秒),默认1小时
"""
self._players: dict[str, dict[str, Any]] = {}
self._lock = threading.RLock()
self.timeoutSeconds = timeoutSeconds
self.cleanupInterval = cleanupInterval
# 启动后台清理线程
self._cleanupThread = threading.Thread(target=self._cleanupWorker, daemon=True)
self._running = True
self._cleanupThread.start()
def createUser(self, uid: str, userObj: Any) -> bool:
"""
创建并管理用户对象
Args:
uid: 用户ID
userObj: 用户对象
Returns:
bool: 是否创建成功
"""
with self._lock:
if uid in self._players:
logger.debug(f"用户 {uid} 已存在,正在覆盖")
# 可以选择返回False或者覆盖,这里选择覆盖
# return False
self._players[uid] = {
"object": userObj,
"lastActive": time.time(),
"activeCount": 0,
}
logger.debug(f"用户 {uid} 创建并开始管理")
return True
def getUser(self, uid: str) -> Any | None:
"""
获取用户对象并刷新活跃时间
Args:
uid: 用户ID
Returns:
Optional[Any]: 用户对象,如果不存在或已超时则返回None
"""
with self._lock:
userData = self._players.get(uid)
if not userData:
logger.debug(f"用户 {uid} 不存在")
return None
# 检查是否已超时(防御性检查)
currentTime = time.time()
if currentTime - userData["lastActive"] > self.timeoutSeconds:
logger.debug(f"用户 {uid} 在获取操作期间已超时")
self._removeUser(uid)
return None
# 刷新活跃时间
userData["lastActive"] = currentTime
userData["activeCount"] += 1
logger.debug(f"用户 {uid} 获取成功,活跃次数: {userData['activeCount']}")
return userData["object"]
def updateUser(self, uid: str, userObj: Any) -> bool:
"""
更新用户对象
Args:
uid: 用户ID
userObj: 新的用户对象
Returns:
bool: 是否更新成功
"""
with self._lock:
if uid not in self._players:
logger.debug(f"用户 {uid} 不存在,无法更新")
return False
self._players[uid]["object"] = userObj
self._players[uid]["lastActive"] = time.time()
logger.debug(f"用户 {uid} 更新成功")
return True
def removeUser(self, uid: str) -> bool:
"""
主动移除用户
Args:
uid: 用户ID
Returns:
bool: 是否移除成功
"""
with self._lock:
return self._removeUser(uid)
def _removeUser(self, uid: str) -> bool:
"""内部移除用户方法"""
if uid in self._players:
userData = self._players.pop(uid)
# 如果需要清理资源,可以在这里处理
if hasattr(userData["object"], "close"):
try:
userData["object"].close()
except Exception as e:
logger.debug(f"关闭用户 {uid} 时出错: {e}")
logger.debug(f"用户 {uid} 已移除,总活跃次数: {userData['activeCount']}")
return True
return False
def _cleanupWorker(self):
"""后台清理线程的工作函数"""
while self._running:
try:
self._cleanupExpiredUsers()
except Exception as e:
logger.debug(f"清理工作线程出错: {e}")
# 休眠指定间隔
time.sleep(self.cleanupInterval)
def _cleanupExpiredUsers(self):
"""清理超时用户"""
currentTime = time.time()
expiredUsers = []
# 首先收集过期的用户ID,避免在迭代中修改字典
with self._lock:
for uid, userData in self._players.items():
if currentTime - userData["lastActive"] > self.timeoutSeconds:
expiredUsers.append(uid)
# 移除过期用户
for uid in expiredUsers:
with self._lock:
# 再次检查,防止在收集和移除之间用户被更新
if (
uid in self._players
and currentTime - self._players[uid]["lastActive"]
> self.timeoutSeconds
):
self._removeUser(uid)
if expiredUsers:
logger.debug(f"已清理 {len(expiredUsers)} 个过期用户: {expiredUsers}")
def getActiveUsers(self) -> dict[str, dict[str, Any]]:
"""
获取当前活跃用户信息
Returns:
Dict: 用户信息字典
"""
with self._lock:
# 返回副本避免外部修改
return {
uid: {
"lastActive": data["lastActive"],
"activeCount": data["activeCount"],
"timeRemaining": self.timeoutSeconds
- (time.time() - data["lastActive"]),
}
for uid, data in self._players.items()
}
def userCount(self) -> int:
"""获取当前用户数量"""
with self._lock:
return len(self._players)
def shutdown(self):
"""关闭用户池,清理资源"""
self._running = False
if self._cleanupThread.is_alive():
self._cleanupThread.join(timeout=5)
# 清理所有用户
with self._lock:
uids = list(self._players.keys())
for uid in uids:
self._removeUser(uid)
logger.debug("用户池关闭完成")
g_pUserPool = CPlayerPool()
+247
View File
@@ -0,0 +1,247 @@
import math
from zhenxun.utils.image_utils import ImageTemplate
from ..utils import config, getToolManager
from . import g_pDBlocator
class CShopManager:
@classmethod
async def getSeedShopImage(
cls, filterKey: str | int = 1, num: int = 1, isVip: int = 0
) -> bytes:
"""获取商店页面
Args:
filterKey (str|int):
- 字符串: 根据关键字筛选种子名称
- 整数: 翻至对应页(无筛选)
num (int, optional): 当 filterKey 为字符串时,用于指定页码。Defaults to 1.
Returns:
bytes: 返回商店图片bytes
"""
# 解析参数:区分筛选关键字和页码
filterStr = None
if isinstance(filterKey, int):
page = filterKey
else:
filterStr = filterKey
page = num
# 表头定义
columnName = [
"-",
"种子名称",
"农场币",
"解锁等级",
"果实单价",
"收获经验",
"收获数量",
"成熟时间(小时)",
"收获次数",
"是否可以上架交易行",
]
# 查询所有可购买作物,并根据筛选关键字过滤
plants = await g_pDBlocator.getPlantManager().listPlants()
filteredPlants = []
# 如果是点券商店
if isVip:
columnName[2] = "点券"
for plant in plants:
# 只留下点券购买的种子
if plant["isVip"] == 0:
continue
# 跳过未解锁购买的种子
if plant["isBuy"] == 0:
continue
# 字符串筛选
if filterStr and filterStr not in plant["name"]:
continue
filteredPlants.append(plant)
else:
for plant in plants:
# 只留下农场币购买的种子
if plant["isVip"] == 1:
continue
# 跳过未解锁购买的种子
if plant["isBuy"] == 0:
continue
# 字符串筛选
if filterStr and filterStr not in plant["name"]:
continue
filteredPlants.append(plant)
# 计算分页
totalCount = len(filteredPlants)
pageCount = math.ceil(totalCount / 15) if totalCount else 1
startIndex = (page - 1) * 15
pageItems = filteredPlants[startIndex : startIndex + 15]
# 构建数据行
dataList = []
for plant in pageItems:
# 图标处理
icon = ""
iconPath = config.g_sResourcePath / f"plant/{plant['name']}/icon.png"
if iconPath.exists():
icon = (iconPath, 33, 33)
# 交易行标记
sell = "可以" if plant["sell"] else "不可以"
dataList.append(
[
icon,
plant["name"], # 种子名称
plant["buy"], # 农场币种子单价
plant["level"], # 解锁等级
plant["price"], # 果实单价
plant["experience"], # 收获经验
plant["harvest"], # 收获数量
plant["time"], # 成熟时间(小时)
plant["crop"], # 收获次数
sell, # 是否可上架交易行
]
)
if isVip:
dataList[-1][2] = plant["vipBuy"] # 点券种子单价
# 页码标题
title = f"种子商店 页数: {page}/{pageCount}"
# 渲染表格并返回图片bytes
result = await ImageTemplate.table_page(
title,
"购买示例:@小真寻 购买种子 大白菜 5",
columnName,
dataList,
)
return result.pic2bytes()
@classmethod
async def buySeed(cls, uid: str, name: str, num: int = 1) -> str:
"""购买种子
Args:
uid (str): 用户Uid
name (str): 植物名称
num (int, optional): 购买数量
Returns:
str:
"""
if num <= 0:
return config.g_sTranslation["buySeed"]["notNum"]
player = await getToolManager().getPlayerByUid(uid)
plantInfo = await g_pDBlocator.getPlantManager().getPlantByName(name)
if not plantInfo or not player:
return config.g_sTranslation["buySeed"]["error"]
level = player.user.get("level", 0)
if level < int(plantInfo["level"]):
return config.g_sTranslation["buySeed"]["noLevel"]
vipSeed = plantInfo.get("isVip", 0) == 1
currencyType = "vipPoint" if vipSeed else "point"
price = int(plantInfo["vipBuy" if vipSeed else "buy"])
totalCost = price * num
currentCurrency = player.user.get(currencyType, 0)
if currentCurrency < totalCost:
return config.g_sTranslation["buySeed"][
f"no{'Vip' if vipSeed else ''}Point"
]
await player.subPoint(currencyType, totalCost)
if not await g_pDBlocator.getUserSeedManager().addUserSeedByUid(uid, name, num):
return config.g_sTranslation["buySeed"]["errorSql"]
success_key = "vipSuccess" if vipSeed else "success"
remaining_currency = currentCurrency - totalCost
return config.g_sTranslation["buySeed"][success_key].format(
name=name, total=totalCost, point=remaining_currency
)
@classmethod
async def sellPlantByUid(cls, uid: str, name: str = "", num: int = 1) -> str:
"""出售作物
Args:
uid (str): 用户Uid
Returns:
str:
"""
if not isinstance(name, str) or name.strip() == "":
name = ""
plant = await g_pDBlocator.getUserPlantManager().getUserPlantByUid(uid)
if not plant:
return config.g_sTranslation["sellPlant"]["no"]
point = 0
totalSold = 0
isAll = num == -1
if name == "":
for plantName, count in plant.items():
isLock = await g_pDBlocator.getUserPlantManager().checkPlantLockByName(
uid, plantName
)
if isLock:
continue
plantInfo = await g_pDBlocator.getUserPlantManager().getPlantByName(
plantName
)
if not plantInfo:
continue
point += plantInfo["price"] * count
await g_pDBlocator.getUserPlantManager().updateUserPlantByName(
uid, plantName, 0
)
else:
if name not in plant:
return config.g_sTranslation["sellPlant"]["error"].format(name=name)
available = plant[name]
sellAmount = available if isAll else min(available, num)
if sellAmount <= 0:
return config.g_sTranslation["sellPlant"]["error1"].format(name=name)
await g_pDBlocator.getUserPlantManager().updateUserPlantByName(
uid, name, available - sellAmount
)
totalSold = sellAmount
if name == "":
totalPoint = point
else:
plantInfo = await g_pDBlocator.getUserPlantManager().getPlantByName(name)
if not plantInfo:
price = 0
else:
price = plantInfo["price"]
totalPoint = totalSold * price
player = await getToolManager().getPlayerByUid(uid)
if not player:
return config.g_sTranslation["basic"]["error"]
currentPoint = player.user.get("point", 0)
await player.addPoint("point", currentPoint + totalPoint)
result = "success1" if name == "" else "success"
return config.g_sTranslation["sellPlant"][result].format(
point=totalPoint, num=currentPoint + totalPoint
)
@@ -0,0 +1,3 @@
from .event import g_pEventManager
__all__ = ["g_pEventManager"]
+123
View File
@@ -0,0 +1,123 @@
import inspect
import time
from zhenxun.services.log import logger
class Signal:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
bound = instance.__dict__.get(self.name)
if bound is None:
bound = _SignalBound()
instance.__dict__[self.name] = bound
return bound
class _SignalBound:
def __init__(self):
self._slots = []
self._onceSlots = []
def connect(self, func=None, *, priority=0):
if func is None:
return lambda f: self.connect(f, priority=priority)
if callable(func) and not any(s[0] == func for s in self._slots):
self._slots.append((func, priority))
self._slots.sort(key=lambda x: -x[1])
return func
def connect_once(self, func=None, *, priority=0):
if func is None:
return lambda f: self.connect_once(f, priority=priority)
if callable(func) and not any(s[0] == func for s in self._onceSlots):
self._onceSlots.append((func, priority))
self._onceSlots.sort(key=lambda x: -x[1])
return func
def disconnect(self, func):
self._slots = [s for s in self._slots if s[0] != func]
self._onceSlots = [s for s in self._onceSlots if s[0] != func]
async def emit(self, *args, **kwargs):
slots = list(self._slots)
onceSlots = list(self._onceSlots)
self._onceSlots.clear()
for slot, _ in slots + onceSlots:
start = time.time()
try:
if inspect.iscoroutinefunction(slot):
await slot(*args, **kwargs)
else:
slot(*args, **kwargs)
logger.debug(
f"【真寻农场】事件槽 {slot.__name__} 执行完成,耗时 {(time.time() - start) * 1000:.2f} ms"
)
except Exception as e:
logger.warning(f"事件槽 {slot.__name__} 触发异常: {e}")
class FarmEventManager:
m_beforePlant = Signal()
"""播种前信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
num (int): 播种数量
"""
m_afterPlant = Signal()
"""播种后信号 每块地播种都会触发该信号
Args:
uid (str): 用户Uid
name (str): 播种种子名称
soilIndex (int): 播种地块索引 从1开始
"""
m_beforeHarvest = Signal()
"""收获前信号
Args:
uid (str): 用户Uid
"""
m_afterHarvest = Signal()
"""收获后信号 每块地收获都会触发该信号
Args:
uid (str): 用户Uid
name (str): 收获作物名称
num (int): 收获数量
soilIndex (int): 收获地块索引 从1开始
"""
m_beforeEradicate = Signal()
"""铲除前信号
Args:
uid (str): 用户Uid
"""
m_afterEradicate = Signal()
"""铲除后信号 每块地铲除都会触发该信号
Args:
uid (str): 用户Uid
soilIndex (index): 铲除地块索引 从1开始
"""
m_beforeExpand = Signal()
m_afterExpand = Signal()
m_beforeSteal = Signal()
m_afterSteal = Signal()
m_dit = Signal()
g_pEventManager = FarmEventManager()
+71
View File
@@ -0,0 +1,71 @@
# 真寻农场更新日志
## V1.5
用户方面
---
- 新增土地升级指令,快来将土地升级至红土地、黑土地、金土地吧
- 新增查漏补缺作物资源功能,自动更新最新作物和作物资源文件
- 定时更新签到文件、作物资源从00:30调整至04:30
- 修正了部分土地资源错误的情况
- 修正了部分文本信息错误的情况
代码方面
---
- 修正部分事件连接机制
- 修正网络请求端口
## V1.4
用户方面
---
- 新增种子商店筛选功能(如果没有BUG的话后续我的种子、我的作物等也会加入筛选功能
- 新增签到功能(测试
- 新增农场详述功能,能通过该功能更加详细的观看农场数据
- 修复了迁移旧数据库无法正常迁移的BUG
- 修复了偷菜会导致偷自己的BUG
- 修正了作物阶段绘制不正确的BUG
代码方面
---
- 修正了多阶段作物成长素材计算逻辑
- 修正了作物数据库字段错乱的问题
- 作物新增offset字段,用于以后偏移坐标和大小(尚未启用,该模式有商议
## V1.3
用户方面
---
- 新增部分作物
- 修复一个严重BUG, 该BUG会导致无法重复偷别人菜的问题
代码方面
---
- 将作物数据从plant.json迁移至resource/db/plant.db中,并将操作json改为操作数据库
## V1.2
用户方面
---
- 修复重大BUG,该BUG会导致用户经验等级计算异常
- 修正种子价格,现在不会以单价的形式购买种子了,而是新增的种子价格
代码方面
---
- 彻底重构数据库,但是不会像V1.1一样导致用户数据丢失
- 迁移部分代码结构,使文件名和代码功能更加匹配
- 加入事件机制,采用类似Qt信号槽机制
## V1.1
用户方面
---
- 完善我的农场图片资源,现在会在左上角显示经验、等级、金币等详细信息了
- 完善出售作物逻辑,现在可以空置作物名称来一键出售全部作物了,也可以选择空置数量来一键出售仓库种指定作物
- 完善播种逻辑,现在可以空置数量来一键播种指定作物了
- 新增更改农场名指令
- 改进对土地开垦条件判断
代码方面
---
- 感谢[quanquan1014](https://github.com/quanquan1014)对农场名称中包含特殊字符的处理。
- 更正数据库写法,但是会导致V1.0用户的作物和种子丢失
## V1.0
世界的起源。
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

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