update # 初始化仓库
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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>.
|
||||||
@@ -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。
|
||||||
|
不要直接部署含已知业务缺陷的迁移基线。
|
||||||
@@ -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 和依赖清单。
|
||||||
|
- 所有目标达到对应门槛才能称为“支持”;未实现目标保持禁用。
|
||||||
@@ -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 进程的第三方依赖
|
||||||
|
版本;冲突必须通过兼容测试或分进程解决。
|
||||||
@@ -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 时间戳,业务日期按配置时区计算;日志记录时区语义。
|
||||||
|
时钟可注入,覆盖午夜、闰日、月切换和重启后的结算。
|
||||||
|
|
||||||
|
核心状态改变必须由事务内显式步骤完成。
|
||||||
|
提交后的消息、图片、统计等副作用失败不撤销已提交业务;
|
||||||
|
需要可靠重试时使用操作结果或持久化任务。事件槽捕获异常不能把
|
||||||
|
必需的资产更新失败隐藏成成功。首期不必新增通用事件总线。
|
||||||
|
|
||||||
|
展示读取业务结果,不在绘图过程中更新余额或土地;
|
||||||
|
图片失败回退文本,不重复执行业务。
|
||||||
@@ -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 权限与其他授权方式。
|
||||||
@@ -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 子模块和发布仓库副本。
|
||||||
@@ -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,旧项目仍保持原样。
|
||||||
@@ -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 中的密钥不写源码;旧源码中暴露的服务令牌需服务端轮换。
|
||||||
@@ -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 用于观察忽略规则;路径无需真实存在。
|
||||||
|
脚本不导入插件、不发网络请求、不读玩家数据库。
|
||||||
@@ -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。
|
||||||
|
- 用户已撤回拉取要求,且要求删除本地克隆;三个本地检出已删除,不再访问远程。
|
||||||
|
- 本轮仅修正迁移副本中的硬编码凭据;业务缺陷记录在审查报告,没有隐式修复。
|
||||||
@@ -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."
|
||||||
|
}
|
||||||
@@ -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。
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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. 文档链接、差异和工具错误路径在最后交付核对中记录到验证结果。
|
||||||
|
|
||||||
|
未实现新业务、未提交或推送、未配置远程发布权限。
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 已完成 | 如实说明,收到删除要求后逐个校验路径并删除 |
|
||||||
|
|
||||||
|
没有尝试绕过用户停止拉取要求,也没有执行远程发布。
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# AstrBot 接入预留位置
|
||||||
|
|
||||||
|
发布目标:https://github.com/Shu-Ying/astrbot_plugin_farm.git
|
||||||
|
负责 AstrBot 事件、消息、配置和生命周期,不导入 NoneBot 或真寻。
|
||||||
|
未来生成 main.py、metadata.yaml、配置描述、依赖和私有核心。
|
||||||
|
本目录尚不可安装;必须验证资源体积及实际安装。
|
||||||
|
远程仓库本轮不再拉取或推送。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 普通 NoneBot 接入预留位置
|
||||||
|
|
||||||
|
发布目标:https://github.com/Shu-Ying/nonebot_plugin_farm.git
|
||||||
|
负责 NoneBot 命令、会话、消息、配置与启停,不导入真寻。
|
||||||
|
发布仓库保留 Python 包构建布局;PyPI 发布流程待实施。
|
||||||
|
本目录尚不可安装。远程仓库本轮不再拉取或推送。
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 真寻接入预留位置
|
||||||
|
|
||||||
|
发布目标:https://github.com/Shu-Ying/zhenxun_plugin_farm.git
|
||||||
|
负责命令、配置、真寻金币、数据目录、权限和生命周期。
|
||||||
|
共享核心通过插件私有路径加载。旧真寻入口仍在 legacy 中,
|
||||||
|
本目录尚不可安装。远程仓库本轮不再拉取或推送。
|
||||||
@@ -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>.
|
||||||
@@ -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">
|
||||||
|
|
||||||
|
[](https://qm.qq.com/q/7hsOD4rOw2)
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
|
你是说可以种地对吧🤔?
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
## 目录
|
||||||
|
- [真寻农场(zhenxun\_plugin\_farm)](#真寻农场zhenxun_plugin_farm)
|
||||||
|
- [目录](#目录)
|
||||||
|
- [农场界面](#农场界面)
|
||||||
|
- [如何安装](#如何安装)
|
||||||
|
- [使用指令](#使用指令)
|
||||||
|
- [更新日志(详细):](#更新日志详细)
|
||||||
|
- [用户方面](#用户方面)
|
||||||
|
- [代码方面](#代码方面)
|
||||||
|
- [待办事宜 `Todo` 列表](#待办事宜-todo-列表)
|
||||||
|
- [关于](#关于)
|
||||||
|
- [致谢](#致谢)
|
||||||
|
- [许可证](#许可证)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 农场界面
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 如何安装
|
||||||
|
|
||||||
|
方法一(推荐):在小真寻后台的插件商店下载即可<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`许可证进行开源
|
||||||
@@ -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)
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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}"
|
||||||
@@ -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()
|
||||||
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|
||||||
|
世界的起源。
|
||||||
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 797 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 721 KiB |
|
After Width: | Height: | Size: 768 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 879 KiB |
|
After Width: | Height: | Size: 676 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 984 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 912 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 100 KiB |