mods that hopefully work for a TechAge-based pack
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,2 @@
|
||||
eye_spy.code-workspace
|
||||
thread from zed.md
|
||||
@@ -0,0 +1,504 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
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 this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser 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 Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "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
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY 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
|
||||
LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey 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 library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library 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
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
|
||||
USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random
|
||||
Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -0,0 +1,23 @@
|
||||
License for Code
|
||||
----------------
|
||||
|
||||
Copyright (C) 2026- cjgray24
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as published by
|
||||
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
License for Textures, Models and Sounds
|
||||
---------------------------------------
|
||||
|
||||
CC-BY-SA 4.0 UNPORTED. Created by cjgray24
|
||||
@@ -0,0 +1,235 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="screenshot.png" alt="Eye Spy HUD" width="640">
|
||||
|
||||
# Eye Spy
|
||||
|
||||
**Real-time node, entity, and item inspection — right on your HUD.**
|
||||
|
||||
[](https://content.luanti.org/packages/cjgray24/eye_spy/)
|
||||
[](LICENSE.txt)
|
||||
[](https://www.luanti.org/)
|
||||
[](locale/)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Eye Spy adds a live HUD that shows you everything about whatever you are looking at — the node name, which tool breaks it fastest, how long digging takes, the current light level, crop growth and soil status, entity health, and more. Every detail is configurable through an in-game editor with live preview, and the HUD updates continuously as you look around.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Screenshots](#screenshots)
|
||||
- [Features](#features)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Commands](#commands)
|
||||
- [Configuration](#configuration)
|
||||
- [Advanced Layout Editor](#advanced-layout-editor)
|
||||
- [Localization](#localization)
|
||||
- [Compatibility](#compatibility)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Screenshots
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="screenshots/hud_growth.png" alt="HUD showing crop growth and soil status" width="400"></td>
|
||||
<td><img src="screenshots/settings_ui.png" alt="Settings editor with live HUD preview" width="400"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Crop growth and soil info</td>
|
||||
<td align="center">In-game settings editor with live preview</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="screenshots/hud_entity.png" alt="HUD showing entity health in hearts" width="400"></td>
|
||||
<td><img src="screenshots/layout_editor.png" alt="Advanced Layout Editor with position and panel sliders" width="400"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Entity health display</td>
|
||||
<td align="center">Advanced Layout Editor</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Features
|
||||
|
||||
### HUD Information
|
||||
|
||||
- **Node name and icon** — description and texture of the node you are pointing at
|
||||
- **Tool requirement** — which tool type breaks the node (Pickaxe, Shovel, Axe, Sword, Hand, Shears), color-coded: green for indicating harvestability with your current tool, orange for correct tool but unharvestable, red for wrong tool and unharvestable.
|
||||
- **Dig time** — how many seconds it takes to break the node with your current tool; ∞ for unbreakable blocks
|
||||
- **Light level** — current ambient light at the target position (0–14)
|
||||
- **Spawn hint** — estimates whether the spot is dark enough for hostile mob spawning, based on a configurable light threshold (off by default)
|
||||
- **Crop growth** — visual progress bar and percentage for farming crops, with stall detection when soil or light conditions are wrong
|
||||
- **Soil status** — wet/dry state and fertility suitability for crop soil; also shows directly when you look at a soil block (part of the growth toggle)
|
||||
- **Liquid info** — liquid type (Water/Lava, etc.), whether it is a source or flowing, and the level value; when enabled, Eye Spy can scan through liquid and still show the target behind it
|
||||
- **Entity health** — current and maximum HP as numeric points or ❤ hearts
|
||||
- **Dropped items** — resolves item entities to their actual name, icon, stack count, durability, and metadata
|
||||
- **Mod origin** — footer shows which mod provides the node or entity. with optional advanced mode which gives it as "modname:technicalname" instead.
|
||||
- **Coordinates** — optional footer display of the target's world position
|
||||
|
||||
### Customization
|
||||
|
||||
- **HUD background color** — full RGB slider control with live preview
|
||||
- **Auto-contrast text** — title text automatically switches between light and dark based on your chosen background color; semantic colors for info, footer, and soil lines are contrast-adjusted automatically
|
||||
- **Manual color spectrum** — 6 independent hue sliders for Title, Info, Footer, Line, Growth, and Soil colors when auto-contrast is off
|
||||
- **7 screen positions** — Top-Middle (default), Top-Left, Top-Right, Middle-Left, Middle-Right, Bottom-Left, Bottom-Right
|
||||
- **Health display format** — numeric Points or ❤ Hearts
|
||||
- **Per-feature toggles** — enable or disable Light Level, Spawn Hint, Liquid Info, Dig Time, Growth, Icons, and Coordinates independently; Liquid Info is a single toggle that controls both liquid line visibility and scan-through targeting
|
||||
- **Footer modes** — Compact (mod name only) or Advanced (mod name + full item identifier)
|
||||
- **Advanced Layout Editor** — 35+ fine-grained sliders for pixel-level positioning of every HUD element
|
||||
|
||||
### Performance
|
||||
|
||||
- **Adjustable refresh rate** — 100 ms default in singleplayer, 150 ms in multiplayer, configurable from 50–5000 ms
|
||||
- **Per-player rate overrides** — server admins can set a different refresh interval for specific players without affecting others
|
||||
- **Multi-layer caching** — target position, render signature, layout geometry, light samples, tool type results, and line colors are all cached to minimize unnecessary work each tick
|
||||
- **HUD auto-recovery** — if HUD elements go blank after a long session, Eye Spy detects and restores them automatically
|
||||
- **Optional performance metrics** — per-stage microsecond timing with avg/p95/max reporting via `/eye_spy_perf`
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
|
||||
1. Place the `eye_spy` folder inside your world's `mods/` directory.
|
||||
2. Enable the mod in the mod selection screen before creating or loading a world, **or** add `load_mod_eye_spy = true` to your `world.mt` file.
|
||||
3. Launch the game — the HUD appears automatically when you join.
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. Look at any node, entity, or dropped item to see the HUD.
|
||||
2. Run `/eye_spy_settings` to open the in-game editor.
|
||||
3. Use the **RGB sliders** to set your preferred background color.
|
||||
4. Use the **HUD Alignment** dropdown to move the HUD to a comfortable position.
|
||||
5. Toggle individual info lines under **Info Lines** to show only what you need.
|
||||
6. Click **Apply Changes** to save.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description | Privilege |
|
||||
|---|---|---|
|
||||
| `/eye_spy_settings` | Opens the settings and live-preview editor | — |
|
||||
| `/eye_spy_rate show` | Shows your effective refresh rate and the server default | — |
|
||||
| `/eye_spy_rate default <ms>` | Sets the server-wide HUD refresh rate (50–5000 ms) | `eye_spy_rate` |
|
||||
| `/eye_spy_rate player <name> <ms\|default>` | Sets or clears a per-player refresh rate override | `eye_spy_rate` |
|
||||
| `/eye_spy_perf show` | Prints timing stats (avg / p95 / max) for all internal stages | `eye_spy_rate` |
|
||||
| `/eye_spy_perf reset` | Clears all collected performance samples | `eye_spy_rate` |
|
||||
| `/eye_spy_perf on` | Enables runtime performance metrics collection | `eye_spy_rate` |
|
||||
| `/eye_spy_perf off` | Disables runtime performance metrics collection | `eye_spy_rate` |
|
||||
| `/eye_spy_toggle` | Toggles your Eye Spy HUD on or off | — |
|
||||
| `/eye_spy_status` | Shows your current HUD status and active refresh rate | — |
|
||||
| `/eye_spy_admin disable <name>` | Suppresses the HUD for a player server-side | `eye_spy_admin` |
|
||||
| `/eye_spy_admin enable <name>` | Lifts a server-side HUD suppression for a player | `eye_spy_admin` |
|
||||
| `/eye_spy_admin show [name]` | Shows HUD status for one or all online players | `eye_spy_admin` |
|
||||
|
||||
> The `eye_spy_rate` and `eye_spy_admin` privileges are granted automatically in singleplayer.
|
||||
|
||||
## Configuration
|
||||
|
||||
These settings can be placed in `minetest.conf` or configured in the server's settings UI. They apply server-wide and take effect on the next server start.
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `eye_spy.show_entities` | `true` | Show entity information in the HUD |
|
||||
| `eye_spy.show_liquids` | `false` | Enable liquid-aware targeting. When Liquid Info is on, Eye Spy scans through liquid, shows a liquid line, and can still display the target behind it |
|
||||
| `eye_spy.always_show_hand_in_creative` | `true` | Always display "Hand" as the tool type in creative mode |
|
||||
| `eye_spy.texture_pack_size` | `None` | Force all HUD icons to a fixed resolution. Use this when textures appear at the wrong size with a custom texture pack. Options: `None`, `16x16px`, `32x32px`, `64x64px`, `128x128px`, `256x256px`, and higher |
|
||||
| `eye_spy.update_interval` | `0.25` | Base update interval in seconds (used as a fallback; the rate system normally controls timing) |
|
||||
| `eye_spy.enable_perf_metrics` | `false` | Enable performance metrics collection at startup |
|
||||
| `eye_spy.show_growth` | `true` | Show crop growth and soil status lines |
|
||||
| `eye_spy.show_icons` | `true` | Show the target icon in the HUD |
|
||||
| `eye_spy.default_show_coords` | `false` | Default coordinates visibility for new players |
|
||||
| `eye_spy.default_show_light_level` | `true` | Default light level visibility for new players |
|
||||
| `eye_spy.default_show_spawn_hint` | `false` | Default spawn hint visibility for new players |
|
||||
| `eye_spy.default_show_liquid_info` | `true` | Default liquid info visibility for new players |
|
||||
| `eye_spy.default_show_dig_time` | `true` | Default dig time visibility for new players |
|
||||
|
||||
Per-player settings (colors, position, toggles, layout) are stored in player metadata and are managed entirely through the in-game `/eye_spy_settings` editor.
|
||||
|
||||
## Advanced Layout Editor
|
||||
|
||||
The Advanced Layout Editor gives you fine-grained control over the exact position and scale of every HUD element. Open it from `/eye_spy_settings` → **Advanced Layout**.
|
||||
|
||||
<img src="screenshots/layout_editor.png" alt="Advanced Layout Editor" width="640">
|
||||
|
||||
The editor is split into three pages:
|
||||
|
||||
| Page | What you can adjust |
|
||||
|---|---|
|
||||
| **Position** | Global X/Y offset, per-element X/Y offsets, margins, icon and text base positions |
|
||||
| **Panel** | Background padding (left/right/top/bottom), extra width/height, background scale % |
|
||||
| **Text** | Scale percentage for Title, Info, Lines, and Footer text independently |
|
||||
|
||||
Use the **Target** dropdown to select which element to adjust (Global, BG, Icon, Title, Subtitle, Lines, or Footer). The **Step** control sets the slider increment — use 1 for precise tweaking and 8 for quick coarse movement. All changes preview live on your HUD as you drag. Use **Reset Target** to restore one element or **Reset All** to return everything to defaults.
|
||||
|
||||
## Localization
|
||||
|
||||
Eye Spy is translated into 8 languages:
|
||||
|
||||
| Language | File |
|
||||
|---|---|
|
||||
| Czech | `locale/eye_spy.cs.tr` |
|
||||
| German | `locale/eye_spy.de.tr` |
|
||||
| Spanish | `locale/eye_spy.es.tr` |
|
||||
| French | `locale/eye_spy.fr.tr` |
|
||||
| Italian | `locale/eye_spy.it.tr` |
|
||||
| Dutch | `locale/eye_spy.nl.tr` |
|
||||
| Polish | `locale/eye_spy.pl.tr` |
|
||||
| Russian | `locale/eye_spy.ru.tr` |
|
||||
|
||||
To contribute a new translation or improve an existing one, copy `locale/eye_spy.template.tr`, rename it to `eye_spy.<lang_code>.tr`, and fill in the translations on the right side of each `=` line.
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Game | Status | Notes |
|
||||
|---|---|---|
|
||||
| Minetest Game | ✅ Full | Pickaxe, Shovel, Axe, Sword, Hand tool detection |
|
||||
| MineClone2 | ✅ Full | Pickaxey, Shovely, Axey, Swordy, Shearsy, Handy detection |
|
||||
| Mineclonia | ✅ Full | Same as MineClone2 |
|
||||
| VoxeLibre | ✅ Full | Same as MineClone2 |
|
||||
|
||||
### Optional dependencies
|
||||
|
||||
| Mod | Purpose |
|
||||
|---|---|
|
||||
| `default` | Standard node and tool definitions (Minetest Game) |
|
||||
| `mcl_core`, `mcl_tools` | Node and tool definitions for MineClone-family games |
|
||||
| `mcl_doc` | Additional MineClone documentation integration |
|
||||
|
||||
Eye Spy has **no required dependencies** and works in any game that follows standard Luanti node conventions.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**HUD not appearing**
|
||||
Run `/eye_spy_settings` and click Apply Changes to force the HUD to reinitialize. Confirm the mod is listed as enabled for your world in `world.mt`.
|
||||
|
||||
**Soil shows as Dry even though water is present**
|
||||
Eye Spy scans for water within 3 blocks of the soil position. If the water source is further away, the farming mod's hydration ABM may not have updated the soil block yet — the HUD will reflect the correct state once it does. Moving the water to be directly adjacent resolves this immediately.
|
||||
|
||||
**Icons appear at the wrong size**
|
||||
Set `eye_spy.texture_pack_size` in `minetest.conf` to match your texture pack's base resolution (for example, `64x64px` for a 64× pack).
|
||||
|
||||
**HUD disappears after a long play session**
|
||||
This is a Luanti HUD element timing quirk. Eye Spy detects the missing elements and restores them automatically, stare at a block or something which should trigger the hud and it should detect when it isn't showing and fix itself. Doesn't impact performance. You can also trigger an immediate recovery by running `/eye_spy_settings` and clicking Cancel.
|
||||
|
||||
**Server performance impact**
|
||||
Use `/eye_spy_rate default <ms>` to raise the server-wide refresh interval. You can also give specific players a higher interval with `/eye_spy_rate player <name> <ms>` without changing the default for everyone else. Disabling unused info lines per-player also reduces work per tick.
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports, pull requests, and translations are all welcome. Please open an issue or PR on [GitHub](https://github.com/cjgray24/eye_spy).
|
||||
|
||||
When reporting a bug, please include:
|
||||
- Your Luanti version (`minetest --version`)
|
||||
- Which game or modpack you are using
|
||||
- Clear steps to reproduce the issue
|
||||
|
||||
## License
|
||||
|
||||
- **Code**: [LGPL v2.1](LICENSE.txt)
|
||||
- **Textures, Models, Sounds**: [CC-BY-SA 4.0](LICENSE.txt)
|
||||
|
||||
## Credits
|
||||
|
||||
Created and maintained by [cjgray24](https://content.luanti.org/users/cjgray24/).
|
||||
@@ -0,0 +1,298 @@
|
||||
-- colors.lua
|
||||
-- Pure color math utilities for eye_spy.
|
||||
-- No minetest API calls: safe to use from any context.
|
||||
|
||||
eye_spy.colors = {}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Clamp
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Clamp v to [minv, maxv].
|
||||
local function clamp(v, minv, maxv)
|
||||
return math.max(minv, math.min(maxv, v))
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- RGB packing / unpacking
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Split a packed 24-bit integer into its R, G, B byte components.
|
||||
local function split_rgb(color)
|
||||
local r = math.floor(color / 65536) % 256
|
||||
local g = math.floor(color / 256) % 256
|
||||
local b = color % 256
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
--- Pack R, G, B byte components into a 24-bit integer.
|
||||
local function join_rgb(r, g, b)
|
||||
return (r * 65536) + (g * 256) + b
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Luminance helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Linearise a single 0–255 channel value for WCAG relative-luminance maths.
|
||||
local function channel_luminance(channel)
|
||||
local c = channel / 255
|
||||
|
||||
if c <= 0.03928 then
|
||||
return c / 12.92
|
||||
end
|
||||
|
||||
return ((c + 0.055) / 1.055) ^ 2.4
|
||||
end
|
||||
|
||||
--- WCAG 2.1 relative luminance of a packed 24-bit RGB integer.
|
||||
local function color_luminance(color)
|
||||
local r, g, b = split_rgb(color)
|
||||
|
||||
return (0.2126 * channel_luminance(r))
|
||||
+ (0.7152 * channel_luminance(g))
|
||||
+ (0.0722 * channel_luminance(b))
|
||||
end
|
||||
|
||||
--- WCAG 2.1 contrast ratio between two packed 24-bit colours.
|
||||
local function contrast_ratio(color_a, color_b)
|
||||
local a_l = color_luminance(color_a)
|
||||
local b_l = color_luminance(color_b)
|
||||
|
||||
return (math.max(a_l, b_l) + 0.05) / (math.min(a_l, b_l) + 0.05)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Colour arithmetic
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Multiply every channel of a packed colour by factor and re-pack.
|
||||
local function scale_color(color, factor)
|
||||
local r, g, b = split_rgb(color)
|
||||
r = clamp(math.floor(r * factor), 0, 255)
|
||||
g = clamp(math.floor(g * factor), 0, 255)
|
||||
b = clamp(math.floor(b * factor), 0, 255)
|
||||
return join_rgb(r, g, b)
|
||||
end
|
||||
|
||||
--- Linear interpolation between two packed colours.
|
||||
-- t = 0 → color_a, t = 1 → color_b.
|
||||
local function blend_color(color_a, color_b, t)
|
||||
local factor = clamp(t or 0.5, 0, 1)
|
||||
local ar, ag, ab = split_rgb(color_a)
|
||||
local br, bg, bb = split_rgb(color_b)
|
||||
|
||||
return join_rgb(
|
||||
math.floor((ar * (1 - factor)) + (br * factor) + 0.5),
|
||||
math.floor((ag * (1 - factor)) + (bg * factor) + 0.5),
|
||||
math.floor((ab * (1 - factor)) + (bb * factor) + 0.5)
|
||||
)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- HSL conversion
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Convert R, G, B bytes to HSL (hue 0–360, saturation 0–1, lightness 0–1).
|
||||
local function rgb_to_hsl(r, g, b)
|
||||
local rn, gn, bn = r / 255, g / 255, b / 255
|
||||
local max_c = math.max(rn, gn, bn)
|
||||
local min_c = math.min(rn, gn, bn)
|
||||
local l = (max_c + min_c) * 0.5
|
||||
|
||||
if max_c == min_c then
|
||||
return 0, 0, l
|
||||
end
|
||||
|
||||
local d = max_c - min_c
|
||||
local s = l > 0.5 and d / (2 - max_c - min_c) or d / (max_c + min_c)
|
||||
local h
|
||||
|
||||
if max_c == rn then
|
||||
h = ((gn - bn) / d) % 6
|
||||
elseif max_c == gn then
|
||||
h = (bn - rn) / d + 2
|
||||
else
|
||||
h = (rn - gn) / d + 4
|
||||
end
|
||||
|
||||
return (h / 6) * 360, s, l
|
||||
end
|
||||
|
||||
-- Hoisted helper for hsl_to_rgb — avoids allocating a new closure per call.
|
||||
-- Converts a hue position p/q at offset t (all normalised 0–1) to a channel value.
|
||||
local function hue2rgb(p, q, t)
|
||||
t = t % 1.0
|
||||
if t < 1 / 6 then return p + (q - p) * 6 * t end
|
||||
if t < 1 / 2 then return q end
|
||||
if t < 2 / 3 then return p + (q - p) * (2 / 3 - t) * 6 end
|
||||
return p
|
||||
end
|
||||
|
||||
--- Convert HSL (hue 0–360, saturation 0–1, lightness 0–1) to R, G, B bytes.
|
||||
local function hsl_to_rgb(h, s, l)
|
||||
if s <= 0 then
|
||||
local v = clamp(math.floor(l * 255 + 0.5), 0, 255)
|
||||
return v, v, v
|
||||
end
|
||||
|
||||
h = h / 360
|
||||
local q = l < 0.5 and l * (1 + s) or l + s - l * s
|
||||
local p = 2 * l - q
|
||||
|
||||
return clamp(math.floor(hue2rgb(p, q, h + 1 / 3) * 255 + 0.5), 0, 255),
|
||||
clamp(math.floor(hue2rgb(p, q, h) * 255 + 0.5), 0, 255),
|
||||
clamp(math.floor(hue2rgb(p, q, h - 1 / 3) * 255 + 0.5), 0, 255)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Background luminance (BT.601 perceived brightness, not WCAG)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Perceived brightness (BT.601) from R, G, B bytes (0–255 range).
|
||||
-- Returns a value in [0, 255].
|
||||
local function get_bg_luminance_rgb(r, g, b)
|
||||
return (r * 0.299) + (g * 0.587) + (b * 0.114)
|
||||
end
|
||||
|
||||
--- Perceived brightness (BT.601) from a 6-character lowercase hex string
|
||||
-- such as "1a1a1b". Returns a value in [0, 255].
|
||||
local function get_bg_luminance(rgb_hex)
|
||||
local r = tonumber(rgb_hex:sub(1, 2), 16) or 26
|
||||
local g = tonumber(rgb_hex:sub(3, 4), 16) or 26
|
||||
local b = tonumber(rgb_hex:sub(5, 6), 16) or 27
|
||||
return get_bg_luminance_rgb(r, g, b)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Foreground selection helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Return whichever of near-black or near-white has the higher contrast ratio
|
||||
-- against bg_color (packed 24-bit integer).
|
||||
local function best_monochrome_for_bg(bg_color)
|
||||
local dark = 0x111111
|
||||
local light = 0xF0F0F0
|
||||
|
||||
if contrast_ratio(light, bg_color) >= contrast_ratio(dark, bg_color) then
|
||||
return light
|
||||
end
|
||||
|
||||
return dark
|
||||
end
|
||||
|
||||
--- Map a value in [0, 255] to a spectrum colour (red → green → blue → white).
|
||||
local function spectrum_color(val)
|
||||
local v = clamp(val or 255, 0, 255)
|
||||
local r, g, b = 0, 0, 0
|
||||
|
||||
if v < 85 then
|
||||
r = 255 - (v * 3)
|
||||
g = v * 3
|
||||
elseif v < 170 then
|
||||
local x = v - 85
|
||||
g = 255 - (x * 3)
|
||||
b = x * 3
|
||||
elseif v < 230 then
|
||||
local x = v - 170
|
||||
r = x * 4
|
||||
b = 255 - (x * 4)
|
||||
else
|
||||
local x = math.floor((v - 230) * (255 / 25))
|
||||
r, g, b = x, x, x
|
||||
end
|
||||
|
||||
return join_rgb(r, g, b)
|
||||
end
|
||||
|
||||
--- Darken color when the background (given as a hex string) is light,
|
||||
-- so the colour remains legible.
|
||||
-- Uses get_bg_luminance_rgb + split_rgb to avoid duplicating the BT.601 formula.
|
||||
local function adapt_for_bg(color, bg_hex)
|
||||
if get_bg_luminance(bg_hex) <= 140 then
|
||||
return color
|
||||
end
|
||||
|
||||
local luminance = get_bg_luminance_rgb(split_rgb(color))
|
||||
|
||||
if luminance > 130 then
|
||||
return scale_color(color, 0.45)
|
||||
end
|
||||
|
||||
return color
|
||||
end
|
||||
|
||||
--- Iteratively blend color toward black or white until it reaches min_ratio
|
||||
-- contrast against bg_color. Makes up to 5 nudge passes.
|
||||
local function nudge_contrast(color, bg_color, min_ratio)
|
||||
local candidate = color
|
||||
local bg_l = color_luminance(bg_color)
|
||||
|
||||
for _ = 1, 5 do
|
||||
if contrast_ratio(candidate, bg_color) >= (min_ratio or 4.5) then
|
||||
return candidate
|
||||
end
|
||||
|
||||
local fg_l = color_luminance(candidate)
|
||||
|
||||
if fg_l < bg_l then
|
||||
candidate = blend_color(candidate, 0x000000, 0.32)
|
||||
else
|
||||
candidate = blend_color(candidate, 0xFFFFFF, 0.32)
|
||||
end
|
||||
end
|
||||
|
||||
return candidate
|
||||
end
|
||||
|
||||
--- Adjust color to meet min_ratio contrast against bg_color while
|
||||
-- preserving hue by blending toward white or black (whichever gives
|
||||
-- the better contrast) in up to 8 steps.
|
||||
local function tune_contrast_preserve_hue(color, bg_color, min_ratio)
|
||||
local required_ratio = min_ratio or 4.0
|
||||
local candidate = color
|
||||
|
||||
if contrast_ratio(candidate, bg_color) >= required_ratio then
|
||||
return candidate
|
||||
end
|
||||
|
||||
for step = 1, 8 do
|
||||
if contrast_ratio(candidate, bg_color) >= required_ratio then
|
||||
return candidate
|
||||
end
|
||||
|
||||
local t = clamp(step * 0.09, 0, 0.70)
|
||||
local lighter = blend_color(color, 0xFFFFFF, t)
|
||||
local darker = blend_color(color, 0x000000, t)
|
||||
|
||||
if contrast_ratio(lighter, bg_color) >= contrast_ratio(darker, bg_color) then
|
||||
candidate = lighter
|
||||
else
|
||||
candidate = darker
|
||||
end
|
||||
end
|
||||
|
||||
return candidate
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Public API
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
eye_spy.colors.clamp = clamp
|
||||
eye_spy.colors.split_rgb = split_rgb
|
||||
eye_spy.colors.join_rgb = join_rgb
|
||||
eye_spy.colors.channel_luminance = channel_luminance
|
||||
eye_spy.colors.color_luminance = color_luminance
|
||||
eye_spy.colors.contrast_ratio = contrast_ratio
|
||||
eye_spy.colors.scale_color = scale_color
|
||||
eye_spy.colors.blend_color = blend_color
|
||||
eye_spy.colors.rgb_to_hsl = rgb_to_hsl
|
||||
eye_spy.colors.hsl_to_rgb = hsl_to_rgb
|
||||
eye_spy.colors.get_bg_luminance_rgb = get_bg_luminance_rgb
|
||||
eye_spy.colors.get_bg_luminance = get_bg_luminance
|
||||
eye_spy.colors.best_monochrome_for_bg = best_monochrome_for_bg
|
||||
eye_spy.colors.spectrum_color = spectrum_color
|
||||
eye_spy.colors.adapt_for_bg = adapt_for_bg
|
||||
eye_spy.colors.nudge_contrast = nudge_contrast
|
||||
eye_spy.colors.tune_contrast_preserve_hue = tune_contrast_preserve_hue
|
||||
@@ -0,0 +1,403 @@
|
||||
--commands
|
||||
|
||||
local S = minetest.get_translator(minetest.get_current_modname())
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Privileges
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_privilege("eye_spy_rate", {
|
||||
description = "Manage Eye Spy HUD refresh rates",
|
||||
give_to_singleplayer = true,
|
||||
})
|
||||
|
||||
minetest.register_privilege("eye_spy_admin", {
|
||||
description = "Manage Eye Spy HUD visibility per player (server-side override).",
|
||||
give_to_singleplayer = true,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Helper: clamp an interval value to the permitted range (mirrors functions.lua)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function clamp_ms(value)
|
||||
return math.max(50, math.min(5000, tonumber(value) or 250))
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- /eye_spy_settings — open the settings UI (no priv required, player-only)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand("eye_spy_settings", {
|
||||
description = S("Open Eye Spy settings and live preview editor."),
|
||||
func = function(name, _param)
|
||||
local player = minetest.get_player_by_name(name)
|
||||
|
||||
if not player then
|
||||
return false, S("This command can only be used by a player.")
|
||||
end
|
||||
|
||||
eye_spy.enter_edit_mode(player)
|
||||
return true, ""
|
||||
end,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- /eye_spy_rate — view/set refresh rate defaults and per-player overrides
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand("eye_spy_rate", {
|
||||
params = "show | default <ms> | player <name> <ms|default>",
|
||||
description = S("Admin: view/set Eye Spy refresh rate defaults and optional per-player overrides"),
|
||||
privs = { eye_spy_rate = true },
|
||||
func = function(name, param)
|
||||
-- Parse
|
||||
local args = {}
|
||||
for token in (param or ""):gmatch("%S+") do
|
||||
args[#args + 1] = token
|
||||
end
|
||||
|
||||
local action = args[1]
|
||||
|
||||
-- show (default)
|
||||
if not action or action == "" or action == "show" then
|
||||
local server_default = eye_spy.get_server_default_interval_ms()
|
||||
local self_effective = eye_spy.get_effective_interval_ms(name)
|
||||
return true, string.format(
|
||||
"Eye Spy rate: server default=%dms, your effective=%dms",
|
||||
server_default,
|
||||
self_effective
|
||||
)
|
||||
end
|
||||
|
||||
-- default <ms>
|
||||
if action == "default" then
|
||||
local ms = tonumber(args[2])
|
||||
|
||||
if not ms then
|
||||
return false, "Usage: /eye_spy_rate default <ms>"
|
||||
end
|
||||
|
||||
local clamped = clamp_ms(ms)
|
||||
eye_spy.set_server_default_interval_ms(clamped)
|
||||
|
||||
-- Apply to all online players that have no personal override
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
local player_name = player:get_player_name()
|
||||
|
||||
if not eye_spy.get_player_interval_override_ms(player_name) then
|
||||
eye_spy.apply_player_interval(player)
|
||||
end
|
||||
end
|
||||
|
||||
return true, string.format("Eye Spy server default refresh set to %dms", clamped)
|
||||
end
|
||||
|
||||
-- player <name> <ms|default>
|
||||
if action == "player" then
|
||||
local target_name = args[2]
|
||||
local value = args[3]
|
||||
|
||||
if not target_name or not value then
|
||||
return false, "Usage: /eye_spy_rate player <name> <ms|default>"
|
||||
end
|
||||
|
||||
if value == "default" then
|
||||
eye_spy.set_player_interval_override_ms(target_name, nil)
|
||||
else
|
||||
local ms = tonumber(value)
|
||||
|
||||
if not ms then
|
||||
return false, "Usage: /eye_spy_rate player <name> <ms|default>"
|
||||
end
|
||||
|
||||
eye_spy.set_player_interval_override_ms(target_name, clamp_ms(ms))
|
||||
end
|
||||
|
||||
local target_player = minetest.get_player_by_name(target_name)
|
||||
|
||||
if target_player then
|
||||
eye_spy.apply_player_interval(target_player)
|
||||
end
|
||||
|
||||
return true, string.format(
|
||||
"Eye Spy rate for %s is now %dms",
|
||||
target_name,
|
||||
eye_spy.get_effective_interval_ms(target_name)
|
||||
)
|
||||
end
|
||||
|
||||
return false, "Usage: /eye_spy_rate show | default <ms> | player <name> <ms|default>"
|
||||
end,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- /eye_spy_perf — show/reset perf stats and enable/disable metrics collection
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand("eye_spy_perf", {
|
||||
params = "show | reset | on | off",
|
||||
description = S("Admin: show/reset Eye Spy perf stats and enable/disable metrics"),
|
||||
privs = { eye_spy_rate = true },
|
||||
func = function(_name, param)
|
||||
-- Parse
|
||||
local action = (param or ""):match("^%s*(%S+)") or "show"
|
||||
|
||||
if action == "show" then
|
||||
return true, eye_spy.perf_report()
|
||||
end
|
||||
|
||||
if action == "reset" then
|
||||
eye_spy.perf_reset()
|
||||
return true, "Eye Spy perf stats reset"
|
||||
end
|
||||
|
||||
if action == "on" then
|
||||
eye_spy.perf_set_enabled(true)
|
||||
return true, "Eye Spy perf metrics enabled"
|
||||
end
|
||||
|
||||
if action == "off" then
|
||||
eye_spy.perf_set_enabled(false)
|
||||
return true, "Eye Spy perf metrics disabled"
|
||||
end
|
||||
|
||||
return false, "Usage: /eye_spy_perf show | reset | on | off"
|
||||
end,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- /eye_spy_toggle — player toggles their own HUD on/off (player-only)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand("eye_spy_toggle", {
|
||||
description = S("Toggle your Eye Spy HUD on or off."),
|
||||
func = function(name, _param)
|
||||
-- Validate: must be an in-game player
|
||||
local player = minetest.get_player_by_name(name)
|
||||
|
||||
if not player then
|
||||
return false, S("This command can only be used by a player.")
|
||||
end
|
||||
|
||||
local meta = player:get_meta()
|
||||
local enabled = meta:get_string("es_hud_enabled") ~= "false"
|
||||
|
||||
if enabled then
|
||||
-- Toggling OFF
|
||||
meta:set_string("es_hud_enabled", "false")
|
||||
|
||||
local state = eye_spy.get_player_state(player)
|
||||
eye_spy.render.hide(state, player)
|
||||
state.last_render_key = nil
|
||||
state.cached_target = nil
|
||||
|
||||
return true, S("Eye Spy HUD disabled.")
|
||||
else
|
||||
-- Toggling ON — save preference regardless of admin block
|
||||
meta:set_string("es_hud_enabled", "true")
|
||||
|
||||
-- Check whether an admin has blocked this player
|
||||
local admin_disabled =
|
||||
eye_spy.storage:get_string("player_disabled:" .. name) == "true"
|
||||
|
||||
if admin_disabled then
|
||||
return true, S(
|
||||
"HUD preference set to enabled, but Eye Spy is currently disabled for you by the server admin."
|
||||
)
|
||||
end
|
||||
|
||||
-- No admin block — HUD will show on the next step() tick
|
||||
return true, S("Eye Spy HUD enabled.")
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- /eye_spy_status — shows the player their current HUD status (player-only)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand("eye_spy_status", {
|
||||
description = S("Show your current Eye Spy HUD status."),
|
||||
func = function(name, _param)
|
||||
-- Validate: must be an in-game player
|
||||
local player = minetest.get_player_by_name(name)
|
||||
|
||||
if not player then
|
||||
return false, S("This command can only be used by a player.")
|
||||
end
|
||||
|
||||
local meta = player:get_meta()
|
||||
local self_disabled = meta:get_string("es_hud_enabled") == "false"
|
||||
local admin_disabled = eye_spy.storage:get_string("player_disabled:" .. name) == "true"
|
||||
local interval_ms = eye_spy.get_effective_interval_ms(name)
|
||||
|
||||
local status_line
|
||||
|
||||
if not self_disabled and not admin_disabled then
|
||||
status_line = S("Eye Spy HUD: active")
|
||||
elseif self_disabled and admin_disabled then
|
||||
status_line = S("Eye Spy HUD: disabled by you and by the server admin")
|
||||
elseif self_disabled then
|
||||
status_line = S("Eye Spy HUD: disabled by you — use /eye_spy_toggle to re-enable")
|
||||
else
|
||||
-- admin_disabled only
|
||||
status_line = S("Eye Spy HUD: disabled by the server admin")
|
||||
end
|
||||
|
||||
return true, string.format("%s\n%s", status_line,
|
||||
string.format(S("Update rate: %dms"), interval_ms))
|
||||
end,
|
||||
})
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- /eye_spy_admin — server-side per-player HUD suppression (eye_spy_admin priv)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
minetest.register_chatcommand("eye_spy_admin", {
|
||||
params = "enable <player> | disable <player> | show [player]",
|
||||
description = S("Manage Eye Spy HUD visibility per player (server-side override)."),
|
||||
privs = { eye_spy_admin = true },
|
||||
func = function(caller, param)
|
||||
-- Parse
|
||||
local args = {}
|
||||
for token in (param or ""):gmatch("%S+") do
|
||||
args[#args + 1] = token
|
||||
end
|
||||
|
||||
local action = args[1]
|
||||
local target_name = args[2]
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- disable <player>
|
||||
-- -----------------------------------------------------------------------
|
||||
if action == "disable" then
|
||||
if not target_name or target_name == "" then
|
||||
return false, "Usage: /eye_spy_admin disable <player>"
|
||||
end
|
||||
|
||||
-- Execute
|
||||
eye_spy.storage:set_string("player_disabled:" .. target_name, "true")
|
||||
|
||||
minetest.log("action", string.format(
|
||||
"[eye_spy] %s disabled Eye Spy HUD for player %s",
|
||||
caller, target_name
|
||||
))
|
||||
|
||||
local target_player = minetest.get_player_by_name(target_name)
|
||||
|
||||
if target_player then
|
||||
-- Immediately hide the HUD
|
||||
local state = eye_spy.get_player_state(target_player)
|
||||
eye_spy.render.hide(state, target_player)
|
||||
state.last_render_key = nil
|
||||
state.cached_target = nil
|
||||
|
||||
minetest.chat_send_player(
|
||||
target_name,
|
||||
S("Eye Spy has been disabled for you by an administrator.")
|
||||
)
|
||||
end
|
||||
|
||||
return true, string.format(
|
||||
"Eye Spy disabled for %s.", target_name
|
||||
)
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- enable <player>
|
||||
-- -----------------------------------------------------------------------
|
||||
if action == "enable" then
|
||||
if not target_name or target_name == "" then
|
||||
return false, "Usage: /eye_spy_admin enable <player>"
|
||||
end
|
||||
|
||||
-- Execute: clear the admin-disable flag
|
||||
eye_spy.storage:set_string("player_disabled:" .. target_name, "")
|
||||
|
||||
minetest.log("action", string.format(
|
||||
"[eye_spy] %s re-enabled Eye Spy HUD for player %s",
|
||||
caller, target_name
|
||||
))
|
||||
|
||||
local target_player = minetest.get_player_by_name(target_name)
|
||||
|
||||
if target_player then
|
||||
-- Let the HUD re-evaluate itself on the next step() tick
|
||||
eye_spy.invalidate_player(target_player)
|
||||
|
||||
minetest.chat_send_player(
|
||||
target_name,
|
||||
S("Eye Spy has been re-enabled for you.")
|
||||
)
|
||||
end
|
||||
|
||||
return true, string.format(
|
||||
"Eye Spy re-enabled for %s.", target_name
|
||||
)
|
||||
end
|
||||
|
||||
-- -----------------------------------------------------------------------
|
||||
-- show [player]
|
||||
-- -----------------------------------------------------------------------
|
||||
if not action or action == "" or action == "show" then
|
||||
if target_name and target_name ~= "" then
|
||||
-- Status for a specific player (may be offline)
|
||||
local admin_disabled = eye_spy.storage:get_string(
|
||||
"player_disabled:" .. target_name
|
||||
) == "true"
|
||||
|
||||
local self_disabled = false
|
||||
local online_note = "(offline — self-disable state unknown)"
|
||||
local target_player = minetest.get_player_by_name(target_name)
|
||||
|
||||
if target_player then
|
||||
local meta = target_player:get_meta()
|
||||
self_disabled = meta:get_string("es_hud_enabled") == "false"
|
||||
online_note = "(online)"
|
||||
end
|
||||
|
||||
return true, string.format(
|
||||
"Eye Spy status for %s %s:\n admin-disabled: %s\n self-disabled: %s",
|
||||
target_name,
|
||||
online_note,
|
||||
admin_disabled and "yes" or "no",
|
||||
target_player and (self_disabled and "yes" or "no") or "unknown"
|
||||
)
|
||||
end
|
||||
|
||||
-- No specific player — list all online players
|
||||
local lines = { "Eye Spy status for online players:" }
|
||||
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
local pname = player:get_player_name()
|
||||
local meta = player:get_meta()
|
||||
local admin_disabled = eye_spy.storage:get_string(
|
||||
"player_disabled:" .. pname
|
||||
) == "true"
|
||||
local self_disabled = meta:get_string("es_hud_enabled") == "false"
|
||||
|
||||
local tag
|
||||
if admin_disabled and self_disabled then
|
||||
tag = "disabled (admin + self)"
|
||||
elseif admin_disabled then
|
||||
tag = "disabled (admin)"
|
||||
elseif self_disabled then
|
||||
tag = "disabled (self)"
|
||||
else
|
||||
tag = "active"
|
||||
end
|
||||
|
||||
lines[#lines + 1] = string.format(" %s — %s", pname, tag)
|
||||
end
|
||||
|
||||
if #lines == 1 then
|
||||
lines[#lines + 1] = " (no players online)"
|
||||
end
|
||||
|
||||
return true, table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
return false, "Usage: /eye_spy_admin enable <player> | disable <player> | show [player]"
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,199 @@
|
||||
-- eye_spy/config.lua
|
||||
-- Loads all mod settings from minetest.settings and exposes them as
|
||||
-- eye_spy.config. Also handles the one-time migration of the legacy
|
||||
-- eye_spy.update_interval (seconds) setting into the ms-based storage key
|
||||
-- used by the interval system defined in functions.lua.
|
||||
--
|
||||
-- Load order requirement: runs AFTER eye_spy = {} is declared in functions.lua,
|
||||
-- so eye_spy, eye_spy.storage, and eye_spy.set_server_default_interval_ms are
|
||||
-- all guaranteed to be available here.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Internal helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Map the human-readable texture_pack_size enum value to a pixel count, or
|
||||
-- nil when the user has not forced a size (i.e. the "None" default).
|
||||
local TEXTURE_SIZE_MAP = {
|
||||
["None"] = nil,
|
||||
["1x1px"] = 1,
|
||||
["2x2px"] = 2,
|
||||
["4x4px"] = 4,
|
||||
["8x8px"] = 8,
|
||||
["16x16px"] = 16,
|
||||
["32x32px"] = 32,
|
||||
["64x64px"] = 64,
|
||||
["128x128px"] = 128,
|
||||
["256x256px"] = 256,
|
||||
["512x512px"] = 512,
|
||||
["1024x1024px"] = 1024,
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Raw setting reads
|
||||
-- (each variable is local to this file; the canonical values live in
|
||||
-- eye_spy.config so the rest of the mod always reads from one place)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local settings = minetest.settings
|
||||
|
||||
-- Texture pack size enum (raw string, used both stored and for derived value)
|
||||
local texture_pack_size_raw = settings:get("eye_spy.texture_pack_size") or "None"
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- eye_spy.config
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Every entry is documented so server admins can understand what each key
|
||||
-- controls by reading this file alone.
|
||||
|
||||
eye_spy.config = {
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Entity / liquid display
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- Whether to show entity info (health, name) when looking at a mob or
|
||||
-- dropped item. Corresponds to eye_spy.show_entities in settings.
|
||||
show_entities = settings:get_bool("eye_spy.show_entities") ~= false,
|
||||
|
||||
-- Whether liquid-aware targeting is active. When true Eye Spy can scan
|
||||
-- through liquid blocks and still show what is behind them.
|
||||
-- Corresponds to eye_spy.show_liquids in settings.
|
||||
show_liquids = settings:get_bool("eye_spy.show_liquids") or false,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Creative / tool display
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- When true the HUD always shows "Hand" as the wielded tool while the
|
||||
-- player is in creative mode, regardless of what is actually held.
|
||||
-- Corresponds to eye_spy.always_show_hand_in_creative in settings.
|
||||
always_show_hand_in_creative = settings:get_bool("eye_spy.always_show_hand_in_creative") ~= false,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Texture / icon sizing
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- The raw enum string chosen by the admin ("None", "16x16px", etc.).
|
||||
-- Stored here for reference; use forced_texture_size for numeric logic.
|
||||
texture_pack_size = texture_pack_size_raw,
|
||||
|
||||
-- Numeric pixel size derived from texture_pack_size, or nil when no size
|
||||
-- is forced. When mod_security is false the engine can determine sizes
|
||||
-- automatically; this override is intended for texture packs that lie
|
||||
-- about their resolution.
|
||||
forced_texture_size = TEXTURE_SIZE_MAP[texture_pack_size_raw],
|
||||
|
||||
-- Absolute path to the active texture pack directory as reported by the
|
||||
-- engine. May be nil when no texture pack is in use. Used to resolve
|
||||
-- icon paths at runtime. Corresponds to texture_path in settings.
|
||||
texture_pack_path = settings:get("texture_path"),
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Security
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- Mirror of secure.enable_security. When true, certain filesystem
|
||||
-- operations (e.g. automatic icon-size detection) are restricted by the
|
||||
-- engine sandbox and Eye Spy falls back to the admin-configured size.
|
||||
mod_security = settings:get_bool("secure.enable_security") == true,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Update interval (legacy float, seconds)
|
||||
-- The authoritative runtime value is stored in mod storage as
|
||||
-- "server_default_interval_ms" and managed by the interval API in
|
||||
-- functions.lua. This field is retained only for the one-time migration
|
||||
-- performed at the bottom of this file.
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- How often (in seconds) the HUD was refreshed under the old single-float
|
||||
-- scheme. Default 0.25 s = 250 ms.
|
||||
-- Corresponds to eye_spy.update_interval in settings.
|
||||
update_interval = tonumber(settings:get("eye_spy.update_interval")) or 0.1,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Performance metrics
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- Whether the lightweight perf-metrics system starts enabled. Can be
|
||||
-- toggled at runtime via /eye_spy_perf without changing this setting.
|
||||
-- Corresponds to eye_spy.enable_perf_metrics in settings.
|
||||
perf_metrics_enabled = settings:get_bool("eye_spy.enable_perf_metrics") or false,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Growth / plant display
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- When true Eye Spy shows growth-stage and soil-status lines for nodes
|
||||
-- that expose supported plant/sapling metadata.
|
||||
-- Corresponds to eye_spy.show_growth in settings.
|
||||
show_growth = settings:get_bool("eye_spy.show_growth") ~= false,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Icon display
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- When true a small icon representing the targeted node or entity is shown
|
||||
-- in the HUD alongside the text lines.
|
||||
-- Corresponds to eye_spy.show_icons in settings.
|
||||
show_icons = settings:get_bool("eye_spy.show_icons") ~= false,
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- Per-player HUD element defaults (applied on first join)
|
||||
-- These values are written into player meta the first time a player joins
|
||||
-- and can be changed per-player at runtime. Changing these settings only
|
||||
-- affects players who have never joined before (or whose meta was reset).
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
-- Show coordinates in the HUD footer.
|
||||
-- Corresponds to eye_spy.default_show_coords in settings.
|
||||
default_show_coords = settings:get_bool("eye_spy.default_show_coords") or false,
|
||||
|
||||
-- Show the ambient light level at the player's position.
|
||||
-- Corresponds to eye_spy.default_show_light_level in settings.
|
||||
default_show_light_level = settings:get_bool("eye_spy.default_show_light_level") ~= false,
|
||||
|
||||
-- Show the mob-spawning hint line (indicates whether the light level is
|
||||
-- low enough for hostile mobs to spawn nearby).
|
||||
-- Corresponds to eye_spy.default_show_spawn_hint in settings.
|
||||
default_show_spawn_hint = settings:get_bool("eye_spy.default_show_spawn_hint") or false,
|
||||
|
||||
-- Show the liquid-info line when looking at or through a liquid node.
|
||||
-- Corresponds to eye_spy.default_show_liquid_info in settings.
|
||||
default_show_liquid_info = settings:get_bool("eye_spy.default_show_liquid_info") ~= false,
|
||||
|
||||
-- Show the estimated dig-time line for the targeted node.
|
||||
-- Corresponds to eye_spy.default_show_dig_time in settings.
|
||||
default_show_dig_time = settings:get_bool("eye_spy.default_show_dig_time") ~= false,
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Legacy update_interval migration
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Older versions of this mod stored the refresh rate as a plain float (in
|
||||
-- seconds) in the Minetest settings file. The current interval system keeps
|
||||
-- its authoritative value in mod storage as "server_default_interval_ms".
|
||||
--
|
||||
-- If the storage key is not yet set (first run after upgrade, or a clean
|
||||
-- install where the admin copied over an old minetest.conf), and the legacy
|
||||
-- setting was given a non-zero value, convert it to milliseconds and write it
|
||||
-- into storage via the public API so the interval system picks it up.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
if eye_spy.storage:get_string("server_default_interval_ms") == "" then
|
||||
local legacy_s = eye_spy.config.update_interval
|
||||
|
||||
-- Only migrate an explicitly configured value: skip the bare default (0.25)
|
||||
-- unless it was intentionally set. We detect "intentionally set" by
|
||||
-- checking whether the key actually appears in minetest.settings.
|
||||
local raw_setting = settings:get("eye_spy.update_interval")
|
||||
|
||||
if raw_setting ~= nil and tonumber(raw_setting) and tonumber(raw_setting) > 0 then
|
||||
local ms = math.floor(legacy_s * 1000 + 0.5)
|
||||
|
||||
minetest.log("action", "[eye_spy] Migrating legacy update_interval setting ("
|
||||
.. tostring(legacy_s) .. "s → " .. tostring(ms) .. "ms) to mod storage.")
|
||||
|
||||
eye_spy.set_server_default_interval_ms(ms)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,853 @@
|
||||
local S = eye_spy.S
|
||||
|
||||
eye_spy.enrichers = {
|
||||
registry = {},
|
||||
}
|
||||
|
||||
local node_tool_state_by_player = {}
|
||||
|
||||
-- Called by init.lua's on_leaveplayer handler to free per-player cached state.
|
||||
function eye_spy.enrichers.on_player_leave(player_name)
|
||||
node_tool_state_by_player[player_name] = nil
|
||||
end
|
||||
|
||||
local function add_line(view_model, id, text, color)
|
||||
view_model.lines[#view_model.lines + 1] = {
|
||||
id = id,
|
||||
text = text,
|
||||
color = color,
|
||||
}
|
||||
end
|
||||
|
||||
local function meta_bool(meta, key, default_value)
|
||||
local raw = meta:get_string(key)
|
||||
|
||||
if raw == "" then
|
||||
return default_value == true
|
||||
end
|
||||
|
||||
return raw ~= "false"
|
||||
end
|
||||
|
||||
local function growth_bar(percent)
|
||||
local slots = 10
|
||||
local p = math.max(0, math.min(100, percent or 0))
|
||||
local filled = math.floor((p / 100) * slots + 0.5)
|
||||
|
||||
return "[" .. string.rep("|", filled) .. string.rep(" ", slots - filled) .. "] " .. p .. "%"
|
||||
end
|
||||
|
||||
local function liquid_name_from_node(name)
|
||||
if not name or name == "" then
|
||||
return S("Unknown")
|
||||
end
|
||||
|
||||
if name:find("water", 1, true) then
|
||||
return S("Water")
|
||||
end
|
||||
|
||||
if name:find("lava", 1, true) then
|
||||
return S("Lava")
|
||||
end
|
||||
|
||||
return eye_spy.readable_name_from_id(name) or S("Unknown")
|
||||
end
|
||||
|
||||
local ETHEREAL_SAPLING_SOILS = {
|
||||
["ethereal:basandra_bush_sapling"] = "ethereal:fiery_dirt",
|
||||
["ethereal:yellow_tree_sapling"] = "group:soil",
|
||||
["ethereal:big_tree_sapling"] = "default:dirt_with_grass",
|
||||
["ethereal:banana_tree_sapling"] = "ethereal:grove_dirt",
|
||||
["ethereal:frost_tree_sapling"] = "ethereal:crystal_dirt",
|
||||
["ethereal:mushroom_sapling"] = "ethereal:mushroom_dirt",
|
||||
["ethereal:mushroom_brown_sapling"] = "ethereal:mushroom_dirt",
|
||||
["ethereal:palm_sapling"] = "default:sand",
|
||||
["ethereal:willow_sapling"] = "ethereal:gray_dirt",
|
||||
["ethereal:redwood_sapling"] = "default:dirt_with_dry_grass",
|
||||
["ethereal:giant_redwood_sapling"] = "default:dirt_with_dry_grass",
|
||||
["ethereal:orange_tree_sapling"] = "ethereal:prairie_dirt",
|
||||
["ethereal:birch_sapling"] = "default:dirt_with_grass",
|
||||
["ethereal:sakura_sapling"] = "ethereal:bamboo_dirt",
|
||||
["ethereal:olive_tree_sapling"] = "ethereal:grove_dirt",
|
||||
["ethereal:lemon_tree_sapling"] = "ethereal:grove_dirt",
|
||||
}
|
||||
|
||||
local function get_current_dig_result(player, nodename)
|
||||
if not player or not nodename or nodename == "" then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local wielded = player:get_wielded_item()
|
||||
local can_dig, dig_time = eye_spy.tool_can_dig_node(wielded, nodename)
|
||||
|
||||
if can_dig then
|
||||
return true, dig_time
|
||||
end
|
||||
|
||||
local hand_can_dig, hand_dig_time = eye_spy.tool_can_dig_node(ItemStack(""), nodename)
|
||||
|
||||
if hand_can_dig then
|
||||
return true, hand_dig_time
|
||||
end
|
||||
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local function get_below_node(pos)
|
||||
if not pos then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local below_pos = { x = pos.x, y = pos.y - 1, z = pos.z }
|
||||
local below = minetest.get_node_or_nil(below_pos)
|
||||
|
||||
if not below then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
return below, minetest.registered_nodes[below.name]
|
||||
end
|
||||
|
||||
local function is_water_node(node, def)
|
||||
local name = node and node.name or ""
|
||||
|
||||
if name == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if name:find("water", 1, true) then
|
||||
return true
|
||||
end
|
||||
|
||||
local source_name = def and def.liquid_alternative_source or ""
|
||||
local flowing_name = def and def.liquid_alternative_flowing or ""
|
||||
|
||||
return source_name:find("water", 1, true) ~= nil or flowing_name:find("water", 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function has_nearby_water(pos, radius)
|
||||
if not pos then
|
||||
return false
|
||||
end
|
||||
|
||||
local scan_radius = math.max(1, tonumber(radius) or 3)
|
||||
|
||||
for dx = -scan_radius, scan_radius do
|
||||
for dz = -scan_radius, scan_radius do
|
||||
for dy = -1, 1 do
|
||||
if dx ~= 0 or dy ~= 0 or dz ~= 0 then
|
||||
local neighbor = minetest.get_node_or_nil({
|
||||
x = pos.x + dx,
|
||||
y = pos.y + dy,
|
||||
z = pos.z + dz,
|
||||
})
|
||||
|
||||
if neighbor then
|
||||
local neighbor_def = minetest.registered_nodes[neighbor.name]
|
||||
|
||||
if is_water_node(neighbor, neighbor_def) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function is_soil_wet(soil_pos, soil_group)
|
||||
if (tonumber(soil_group) or 0) >= 3 then
|
||||
return true
|
||||
end
|
||||
|
||||
return has_nearby_water(soil_pos, 3)
|
||||
end
|
||||
|
||||
local function get_node_label(name)
|
||||
if not name or name == "" then
|
||||
return S("Unknown")
|
||||
end
|
||||
|
||||
if name:sub(1, 6) == "group:" then
|
||||
local group_name = name:sub(7)
|
||||
|
||||
if group_name == "soil" then
|
||||
return S("Soil")
|
||||
end
|
||||
|
||||
return group_name
|
||||
end
|
||||
|
||||
local def = minetest.registered_nodes[name]
|
||||
local desc = def and def.description
|
||||
|
||||
if desc and desc ~= "" then
|
||||
return desc:match("^[^\n]*") or desc
|
||||
end
|
||||
|
||||
return name
|
||||
end
|
||||
|
||||
local function get_soil_state(pos)
|
||||
local below, below_def = get_below_node(pos)
|
||||
local soil_group = below and minetest.get_item_group(below.name, "soil") or 0
|
||||
local below_pos = pos and { x = pos.x, y = pos.y - 1, z = pos.z } or nil
|
||||
local is_wet = soil_group > 0 and is_soil_wet(below_pos, soil_group)
|
||||
|
||||
return {
|
||||
node = below,
|
||||
def = below_def,
|
||||
soil_group = soil_group,
|
||||
is_soil = soil_group > 0,
|
||||
is_wet = is_wet,
|
||||
}
|
||||
end
|
||||
|
||||
local function get_target_soil_state(target)
|
||||
local soil_group = minetest.get_item_group(target.name or "", "soil")
|
||||
|
||||
if soil_group <= 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
node = {
|
||||
name = target.name,
|
||||
param1 = target.node and target.node.param1 or 0,
|
||||
param2 = target.node and target.node.param2 or 0,
|
||||
},
|
||||
def = target.def,
|
||||
soil_group = soil_group,
|
||||
is_soil = true,
|
||||
is_wet = is_soil_wet(target.pos, soil_group),
|
||||
}
|
||||
end
|
||||
|
||||
local function infer_max_stage(base_name, current_stage)
|
||||
local max_stage = current_stage or 0
|
||||
|
||||
for stage = max_stage + 1, 16 do
|
||||
if minetest.registered_nodes[base_name .. "_" .. stage] then
|
||||
max_stage = stage
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if max_stage > 0 then
|
||||
return max_stage
|
||||
end
|
||||
end
|
||||
|
||||
local function get_registered_crop_info(base_name)
|
||||
if farming and farming.registered_plants and farming.registered_plants[base_name] then
|
||||
local plant = farming.registered_plants[base_name]
|
||||
|
||||
return {
|
||||
system = "farming",
|
||||
steps = plant.steps,
|
||||
minlight = plant.minlight or (farming and farming.min_light) or 12,
|
||||
maxlight = plant.maxlight or (farming and farming.max_light) or 15,
|
||||
}
|
||||
end
|
||||
|
||||
if x_farming and x_farming.registered_plants then
|
||||
local short_name = base_name:match(":(.+)$")
|
||||
local plant = short_name and x_farming.registered_plants[short_name]
|
||||
|
||||
if plant then
|
||||
return {
|
||||
system = "x_farming",
|
||||
steps = plant.steps,
|
||||
minlight = plant.minlight or 13,
|
||||
maxlight = plant.maxlight or 14,
|
||||
fertility = plant.fertility,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function soil_matches_fertility(soil_state, fertility)
|
||||
if not fertility or #fertility == 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local below_def = soil_state.def
|
||||
local groups = below_def and below_def.groups or nil
|
||||
|
||||
if not groups then
|
||||
return false
|
||||
end
|
||||
|
||||
for _, group_name in ipairs(fertility) do
|
||||
if groups[group_name] and groups[group_name] > 0 then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function get_growth_light(pos)
|
||||
if not pos then
|
||||
return nil
|
||||
end
|
||||
|
||||
return minetest.get_node_light(pos)
|
||||
end
|
||||
|
||||
local function get_growth_info(target)
|
||||
local def = target.def
|
||||
local groups = def and def.groups or {}
|
||||
|
||||
if groups.sapling then
|
||||
local meta = minetest.get_meta(target.pos)
|
||||
|
||||
return {
|
||||
kind = "sapling",
|
||||
age = meta and meta:get_int("age") or 0,
|
||||
groups = groups,
|
||||
}
|
||||
end
|
||||
|
||||
local stage_suffix = target.name:match("_(%d+)$")
|
||||
local base_name = stage_suffix and target.name:gsub("_(%d+)$", "", 1) or nil
|
||||
local plant = base_name and get_registered_crop_info(base_name) or nil
|
||||
|
||||
if not stage_suffix or not (groups.plant or groups.crop or groups.growing or plant) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local stage = tonumber(stage_suffix) or 0
|
||||
plant = plant or {}
|
||||
local max_stage = plant.steps or infer_max_stage(base_name, stage)
|
||||
|
||||
if not max_stage or max_stage <= 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return {
|
||||
kind = "crop",
|
||||
system = plant.system or "generic",
|
||||
stage = stage,
|
||||
max_stage = max_stage,
|
||||
minlight = plant.minlight or def.minlight,
|
||||
maxlight = plant.maxlight or def.maxlight,
|
||||
fertility = plant.fertility,
|
||||
}
|
||||
end
|
||||
|
||||
local function get_sapling_soil_requirement(target)
|
||||
local requirement = ETHEREAL_SAPLING_SOILS[target.name]
|
||||
|
||||
if requirement then
|
||||
return requirement
|
||||
end
|
||||
|
||||
return "group:soil"
|
||||
end
|
||||
|
||||
local function soil_matches_requirement(soil_state, requirement)
|
||||
if not requirement or requirement == "" then
|
||||
return true
|
||||
end
|
||||
|
||||
if requirement:sub(1, 6) == "group:" then
|
||||
return minetest.get_item_group(soil_state.node and soil_state.node.name or "", requirement:sub(7)) > 0
|
||||
end
|
||||
|
||||
return soil_state.node and soil_state.node.name == requirement or false
|
||||
end
|
||||
|
||||
local function get_crop_conditions(target, growth_info)
|
||||
local soil_state = get_soil_state(target.pos)
|
||||
local light = target.light_level
|
||||
|
||||
if light == nil then
|
||||
light = get_growth_light(target.pos)
|
||||
end
|
||||
|
||||
local minlight = growth_info.minlight
|
||||
local maxlight = growth_info.maxlight
|
||||
local fertility_ok = soil_matches_fertility(soil_state, growth_info.fertility)
|
||||
local soil_ok = soil_state.is_wet and fertility_ok
|
||||
local light_ok = true
|
||||
|
||||
if minlight and light and light < minlight then
|
||||
light_ok = false
|
||||
end
|
||||
|
||||
if maxlight and light and light > maxlight then
|
||||
light_ok = false
|
||||
end
|
||||
|
||||
if light == nil and (minlight or maxlight) then
|
||||
light_ok = false
|
||||
end
|
||||
|
||||
return {
|
||||
soil = soil_state,
|
||||
light = light,
|
||||
soil_ok = soil_ok,
|
||||
light_ok = light_ok,
|
||||
fertility_ok = fertility_ok,
|
||||
}
|
||||
end
|
||||
|
||||
local function get_soil_line(target, growth_info)
|
||||
local soil_state = get_soil_state(target.pos)
|
||||
local target_groups = target.def and target.def.groups or {}
|
||||
|
||||
if growth_info and growth_info.kind == "crop" then
|
||||
local suitable = soil_state.is_soil and soil_matches_fertility(soil_state, growth_info.fertility)
|
||||
local moisture_label
|
||||
|
||||
if soil_state.is_wet then
|
||||
moisture_label = S("Wet")
|
||||
elseif soil_state.is_soil then
|
||||
moisture_label = S("Dry")
|
||||
else
|
||||
moisture_label = S("No Soil")
|
||||
end
|
||||
|
||||
local color
|
||||
|
||||
if suitable and soil_state.is_wet then
|
||||
color = 0x6BD66B
|
||||
elseif suitable then
|
||||
color = 0xFFAA55
|
||||
else
|
||||
color = 0xE56363
|
||||
end
|
||||
|
||||
return {
|
||||
text = S("Soil") .. ": " .. moisture_label .. " | " .. (suitable and S("Suitable") or S("Unsuitable")),
|
||||
color = color,
|
||||
}
|
||||
end
|
||||
|
||||
if growth_info and growth_info.kind == "sapling" then
|
||||
local requirement = get_sapling_soil_requirement(target)
|
||||
local suitable = soil_matches_requirement(soil_state, requirement)
|
||||
|
||||
return {
|
||||
text = S("Soil") ..
|
||||
": " .. get_node_label(requirement) .. " | " .. (suitable and S("Suitable") or S("Unsuitable")),
|
||||
color = suitable and 0x6BD66B or 0xE56363,
|
||||
}
|
||||
end
|
||||
|
||||
if target_groups.flora or target_groups.attached_node then
|
||||
return nil
|
||||
end
|
||||
|
||||
local target_soil_state = get_target_soil_state(target)
|
||||
|
||||
if target_soil_state then
|
||||
return {
|
||||
text = S("Soil") .. ": " .. (target_soil_state.is_wet and S("Wet") or S("Dry")),
|
||||
color = target_soil_state.is_wet and 0x5555FF or 0xFFAA55,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function eye_spy.enrichers.register(id, spec)
|
||||
eye_spy.enrichers.registry[#eye_spy.enrichers.registry + 1] = {
|
||||
id = id,
|
||||
enabled = spec.enabled,
|
||||
apply = spec.apply,
|
||||
}
|
||||
end
|
||||
|
||||
local function ensure_shared_context(context, target)
|
||||
local shared = context.shared
|
||||
|
||||
if shared and shared._target_key == (target.key or target.name) then
|
||||
return shared
|
||||
end
|
||||
|
||||
shared = {
|
||||
_target_key = target.key or target.name,
|
||||
dig_cached = false,
|
||||
growth_cached = false,
|
||||
conditions_cached = false,
|
||||
soil_line_cached = false,
|
||||
}
|
||||
|
||||
function shared:get_dig_result()
|
||||
if not self.dig_cached then
|
||||
self.dig_cached = true
|
||||
self.can_dig, self.dig_time = get_current_dig_result(context.player, target.name)
|
||||
end
|
||||
|
||||
return self.can_dig, self.dig_time
|
||||
end
|
||||
|
||||
function shared:get_growth_info()
|
||||
if not self.growth_cached then
|
||||
self.growth_cached = true
|
||||
self.growth_info = get_growth_info(target)
|
||||
end
|
||||
|
||||
return self.growth_info
|
||||
end
|
||||
|
||||
function shared:get_crop_conditions()
|
||||
if not self.conditions_cached then
|
||||
self.conditions_cached = true
|
||||
local growth_info = self:get_growth_info()
|
||||
|
||||
if growth_info and growth_info.kind == "crop" then
|
||||
self.crop_conditions = get_crop_conditions(target, growth_info)
|
||||
else
|
||||
self.crop_conditions = nil
|
||||
end
|
||||
end
|
||||
|
||||
return self.crop_conditions
|
||||
end
|
||||
|
||||
function shared:get_soil_line()
|
||||
if not self.soil_line_cached then
|
||||
self.soil_line_cached = true
|
||||
self.soil_line = get_soil_line(target, self:get_growth_info())
|
||||
end
|
||||
|
||||
return self.soil_line
|
||||
end
|
||||
|
||||
context.shared = shared
|
||||
return shared
|
||||
end
|
||||
|
||||
local function get_cached_node_tool(context, target)
|
||||
local player = context and context.player
|
||||
|
||||
if not player then
|
||||
return nil
|
||||
end
|
||||
|
||||
local shared = ensure_shared_context(context, target)
|
||||
local player_name = player:get_player_name()
|
||||
local wielded = player:get_wielded_item()
|
||||
local wield_name = wielded:get_name() or ""
|
||||
local target_key = target.key or target.name or ""
|
||||
local cached = node_tool_state_by_player[player_name]
|
||||
|
||||
if cached and cached.target_key == target_key and cached.wield_name == wield_name then
|
||||
return cached.tool
|
||||
end
|
||||
|
||||
local can_dig = shared:get_dig_result()
|
||||
local tool_type_hint, matches_tool_type = nil, false
|
||||
|
||||
if eye_spy.get_node_tool_hint_and_match then
|
||||
tool_type_hint, matches_tool_type = eye_spy.get_node_tool_hint_and_match(target.name, wielded)
|
||||
end
|
||||
|
||||
local label = tool_type_hint or S("Unknown")
|
||||
local color
|
||||
|
||||
if can_dig then
|
||||
color = 0x6BD66B
|
||||
elseif matches_tool_type then
|
||||
color = 0xFFAA55
|
||||
else
|
||||
color = 0xE56363
|
||||
end
|
||||
|
||||
local tool = {
|
||||
description = label,
|
||||
color = color,
|
||||
}
|
||||
|
||||
node_tool_state_by_player[player_name] = {
|
||||
target_key = target_key,
|
||||
wield_name = wield_name,
|
||||
tool = tool,
|
||||
}
|
||||
|
||||
return tool
|
||||
end
|
||||
|
||||
function eye_spy.enrichers.run(context, target, view_model)
|
||||
ensure_shared_context(context, target)
|
||||
|
||||
for _, enricher in ipairs(eye_spy.enrichers.registry) do
|
||||
if not enricher.enabled or enricher.enabled(context, target, view_model) then
|
||||
local perf_on = eye_spy.perf and eye_spy.perf.enabled and minetest.get_us_time
|
||||
local start_us = perf_on and minetest.get_us_time() or 0
|
||||
|
||||
local ok, err = pcall(enricher.apply, context, target, view_model)
|
||||
if not ok then
|
||||
minetest.log("error", "[eye_spy] enricher '" .. (enricher.id or "?") ..
|
||||
"' error: " .. tostring(err))
|
||||
end
|
||||
|
||||
if perf_on then
|
||||
eye_spy.perf_record("enricher_" .. (enricher.id or "unknown"), minetest.get_us_time() - start_us)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
eye_spy.enrichers.register("node_tool", {
|
||||
enabled = function(_, target)
|
||||
return target.kind == "node"
|
||||
end,
|
||||
apply = function(context, target, view_model)
|
||||
local tool = get_cached_node_tool(context, target) or {
|
||||
description = S("Unknown"),
|
||||
color = 0xEBB344,
|
||||
}
|
||||
|
||||
view_model.subtitle = tool.description ~= "" and tool.description or S("Unknown")
|
||||
view_model.subtitle_color = tool.color or 0xEBB344
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("entity_health", {
|
||||
enabled = function(_, target)
|
||||
return target.kind == "entity"
|
||||
end,
|
||||
apply = function(context, target, view_model)
|
||||
if context.meta:get_string("es_hud_health_in") == "Hearts" then
|
||||
view_model.subtitle = S("Health") ..
|
||||
": " ..
|
||||
(eye_spy.round1((target.hp or 0) / 2) or S("Unknown")) ..
|
||||
"♥/" .. (eye_spy.round1((target.max_hp or 0) / 2) or S("Unknown")) .. "♥"
|
||||
else
|
||||
view_model.subtitle = S("Health") ..
|
||||
": " .. (eye_spy.round1(target.hp) or S("Unknown")) .. "/" .. (eye_spy.round1(target.max_hp) or S("Unknown"))
|
||||
end
|
||||
|
||||
view_model.subtitle_color = 0xFF4040
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("item_details", {
|
||||
enabled = function(_, target)
|
||||
return target.kind == "item"
|
||||
end,
|
||||
apply = function(_, target, view_model)
|
||||
local count = tonumber(target.stack_count) or 1
|
||||
|
||||
if count > 1 then
|
||||
add_line(view_model, "item_count", S("Count") .. ": " .. tostring(count), 0xE8D37A)
|
||||
end
|
||||
|
||||
local wear = tonumber(target.stack_wear) or 0
|
||||
|
||||
if wear > 0 then
|
||||
local remain_pct = math.floor(math.max(0, 1 - (wear / 65535)) * 100 + 0.5)
|
||||
local wear_color = 0xE56363
|
||||
|
||||
if remain_pct >= 60 then
|
||||
wear_color = 0x6BD66B
|
||||
elseif remain_pct >= 30 then
|
||||
wear_color = 0xF0B35E
|
||||
end
|
||||
|
||||
add_line(view_model, "item_wear", S("Durability") .. ": " .. tostring(remain_pct) .. "%", wear_color)
|
||||
end
|
||||
|
||||
if target.item_def_description and target.item_def_description ~= "" then
|
||||
local lines = {}
|
||||
for line in target.item_def_description:gmatch("[^\n]+") do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
for i = 2, #lines do
|
||||
add_line(view_model, "item_def_line_" .. i, lines[i], 0xCCCCCC)
|
||||
end
|
||||
end
|
||||
|
||||
if target.stack_has_meta then
|
||||
local detail = target.stack_meta_summary
|
||||
|
||||
if detail and detail ~= "" then
|
||||
add_line(view_model, "item_meta", S("Metadata") .. ": " .. detail, 0x98C9FF)
|
||||
else
|
||||
add_line(view_model, "item_meta", S("Metadata"), 0x98C9FF)
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("growth", {
|
||||
enabled = function(context, target)
|
||||
return target.kind == "node" and meta_bool(context.meta, "es_show_growth", eye_spy.config.show_growth)
|
||||
end,
|
||||
apply = function(context, target, view_model)
|
||||
local shared = ensure_shared_context(context, target)
|
||||
local growth_info = shared:get_growth_info()
|
||||
|
||||
if not growth_info then
|
||||
return
|
||||
end
|
||||
|
||||
if growth_info.kind == "sapling" then
|
||||
local age = growth_info.age or 0
|
||||
|
||||
if age > 0 then
|
||||
local percent = math.min(math.floor((age / 20) * 100), 100)
|
||||
add_line(view_model, "growth", S("Maturity") .. ": " .. growth_bar(percent), 0xAAAAFF)
|
||||
else
|
||||
add_line(view_model, "growth", S("Status") .. ": " .. S("Rooting"), 0x888888)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
if growth_info.kind == "crop" then
|
||||
local percent = math.floor(math.min(growth_info.stage / math.max(growth_info.max_stage, 1), 1) * 100)
|
||||
local conditions = shared:get_crop_conditions() or get_crop_conditions(target, growth_info)
|
||||
local stalled = growth_info.stage < growth_info.max_stage and
|
||||
(not conditions.soil_ok or not conditions.light_ok)
|
||||
local status = ""
|
||||
|
||||
if stalled then
|
||||
local reasons = {}
|
||||
|
||||
if not conditions.soil_ok then
|
||||
if not conditions.soil.is_wet then
|
||||
-- Soil is dry (and may also be wrong type)
|
||||
reasons[#reasons + 1] = conditions.soil.is_soil and S("Dry Soil") or S("Bad Soil")
|
||||
else
|
||||
-- Soil is wet but wrong fertility type for this crop
|
||||
reasons[#reasons + 1] = S("Bad Soil")
|
||||
end
|
||||
end
|
||||
|
||||
if not conditions.light_ok then
|
||||
reasons[#reasons + 1] = S("Bad Light")
|
||||
end
|
||||
|
||||
status = " (" .. S("Stalled")
|
||||
|
||||
if #reasons > 0 then
|
||||
status = status .. ": " .. table.concat(reasons, ", ")
|
||||
end
|
||||
|
||||
status = status .. ")"
|
||||
end
|
||||
|
||||
add_line(
|
||||
view_model,
|
||||
"growth",
|
||||
S("Growth") .. ": " .. growth_bar(percent) .. status,
|
||||
stalled and 0xFFAA55 or 0x55FF55
|
||||
)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("dig_time", {
|
||||
enabled = function(context, target)
|
||||
return target.kind == "node" and meta_bool(context.meta, "es_show_dig_time", true)
|
||||
end,
|
||||
apply = function(context, target, view_model)
|
||||
local shared = ensure_shared_context(context, target)
|
||||
local diggable, time = shared:get_dig_result()
|
||||
|
||||
if not diggable then
|
||||
add_line(view_model, "dig_time", S("Dig Time") .. ": ∞", 0xA9C5FF)
|
||||
return
|
||||
end
|
||||
|
||||
local t = tonumber(time) or 0
|
||||
add_line(view_model, "dig_time", S("Dig Time") .. ": " .. string.format("%.2fs", t), 0xA9C5FF)
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("soil", {
|
||||
enabled = function(context, target)
|
||||
return target.kind == "node" and meta_bool(context.meta, "es_show_growth", eye_spy.config.show_growth)
|
||||
end,
|
||||
apply = function(context, target, view_model)
|
||||
local shared = ensure_shared_context(context, target)
|
||||
local soil_line = shared:get_soil_line()
|
||||
|
||||
if not soil_line then
|
||||
return
|
||||
end
|
||||
|
||||
add_line(view_model, "soil", soil_line.text, soil_line.color)
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("light_level", {
|
||||
enabled = function(context, target)
|
||||
return target.kind == "node" and meta_bool(context.meta, "es_show_light_level", true)
|
||||
end,
|
||||
apply = function(_, target, view_model)
|
||||
if target.light_level == nil then
|
||||
add_line(view_model, "light", S("Light") .. ": " .. S("Unknown"), 0xAAAAAA)
|
||||
return
|
||||
end
|
||||
|
||||
add_line(view_model, "light", S("Light") .. ": " .. tostring(target.light_level), 0xF2DD88)
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("spawn_risk", {
|
||||
enabled = function(context, target)
|
||||
return target.kind == "node" and meta_bool(context.meta, "es_show_spawn_hint", false)
|
||||
end,
|
||||
apply = function(context, target, view_model)
|
||||
if target.light_level == nil then
|
||||
add_line(view_model, "spawn", S("Spawn Risk") .. " (" .. S("Est") .. "): " .. S("Unknown"), 0xAAAAAA)
|
||||
return
|
||||
end
|
||||
|
||||
local threshold = context.meta:get_int("es_spawn_safe_light_threshold")
|
||||
|
||||
if threshold < 0 or threshold > 15 then
|
||||
threshold = 8
|
||||
end
|
||||
|
||||
if target.light_level <= threshold then
|
||||
add_line(view_model, "spawn", S("Spawn Risk") .. " (" .. S("Est") .. "): " .. S("Dark Enough"), 0xF08A55)
|
||||
else
|
||||
add_line(view_model, "spawn", S("Spawn Risk") .. " (" .. S("Est") .. "): " .. S("Likely Safe"), 0x6BD66B)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
eye_spy.enrichers.register("liquid_info", {
|
||||
enabled = function(context, target)
|
||||
if not eye_spy.config.show_liquids then
|
||||
return false
|
||||
end
|
||||
|
||||
local show_liquid = meta_bool(context.meta, "es_show_liquid_info", true)
|
||||
|
||||
if not show_liquid then
|
||||
return false
|
||||
end
|
||||
|
||||
return target.is_liquid or target.liquid_obstacle ~= nil
|
||||
end,
|
||||
apply = function(_, target, view_model)
|
||||
local liquid_target = target.is_liquid and target or target.liquid_obstacle
|
||||
|
||||
if not liquid_target then
|
||||
return
|
||||
end
|
||||
|
||||
local name = liquid_name_from_node(liquid_target.full_name or liquid_target.name)
|
||||
local detail
|
||||
|
||||
if liquid_target.liquid_type == "source" then
|
||||
detail = S("Source")
|
||||
elseif liquid_target.liquid_level then
|
||||
detail = S("Level") .. ": " .. tostring(liquid_target.liquid_level)
|
||||
else
|
||||
detail = S("Flowing")
|
||||
end
|
||||
|
||||
add_line(view_model, "liquid", S("Liquid") .. ": " .. name .. " (" .. detail .. ")", 0x6FB8FF)
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,676 @@
|
||||
--functions
|
||||
|
||||
eye_spy = {}
|
||||
local S = minetest.get_translator(minetest.get_current_modname())
|
||||
eye_spy.S = S
|
||||
-- eye_spy.cg is set in init.lua after game detection
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Mod storage (called ONCE at load time)
|
||||
------------------------------------------------------------
|
||||
eye_spy.storage = minetest.get_mod_storage()
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Shared runtime tables
|
||||
------------------------------------------------------------
|
||||
eye_spy.state = { players = {} }
|
||||
eye_spy.cache = { tool_info = {} }
|
||||
eye_spy.edit_mode = {}
|
||||
eye_spy.pending_preview_updates = {}
|
||||
eye_spy.pending_layout_refreshes = {}
|
||||
eye_spy.ui_drafts = {}
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Shared helper functions
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Returns a human-readable name from a node/item id string, or nil on failure.
|
||||
-- Callers are responsible for providing their own fallback string.
|
||||
function eye_spy.readable_name_from_id(name)
|
||||
if not name or name == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local raw = tostring(name)
|
||||
local stripped = raw:match(":(.+)$") or raw
|
||||
|
||||
stripped = stripped
|
||||
:gsub("_source$", "")
|
||||
:gsub("_flowing$", "")
|
||||
:gsub("[_%-%./]+", " ")
|
||||
:gsub("%s+", " ")
|
||||
:gsub("^%s+", "")
|
||||
:gsub("%s+$", "")
|
||||
|
||||
if stripped == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
return (stripped:gsub("(%a)([%w']*)", function(a, b)
|
||||
return string.upper(a) .. string.lower(b)
|
||||
end))
|
||||
end
|
||||
|
||||
-- Returns the mod portion of a namespaced id (e.g. "default" from "default:stone").
|
||||
function eye_spy.get_modname_from_obj(obj_name)
|
||||
if not obj_name then return nil end
|
||||
return obj_name:match("([^:]+)")
|
||||
end
|
||||
|
||||
-- Rounds x to one decimal place, or returns nil if x is falsy.
|
||||
function eye_spy.round1(x)
|
||||
if x then
|
||||
return math.floor(x * 10 + 0.5) / 10
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Strips everything from the first texture-modifier caret (^) onwards.
|
||||
function eye_spy.strip_texture_modifiers(tex)
|
||||
if type(tex) ~= "string" then
|
||||
return tex
|
||||
end
|
||||
|
||||
local pos = tex:find("%^")
|
||||
if not pos then
|
||||
return tex
|
||||
end
|
||||
|
||||
return tex:sub(1, pos - 1)
|
||||
end
|
||||
|
||||
-- Parses an "R, G, B" string and returns three numbers.
|
||||
-- Falls back to 26, 26, 27 for any component that cannot be parsed.
|
||||
function eye_spy.get_rgb(v)
|
||||
if not v or v == "" then
|
||||
return 26, 26, 27
|
||||
end
|
||||
|
||||
v = v:gsub("%s", "")
|
||||
|
||||
local r, g, b = v:match("^(%d+),(%d+),(%d+)$")
|
||||
r, g, b = tonumber(r), tonumber(g), tonumber(b)
|
||||
|
||||
return r or 26, g or 26, b or 27
|
||||
end
|
||||
|
||||
-- Converts three 0-255 channel values to an uppercase hex string.
|
||||
function eye_spy.rgb_to_hex(r, g, b)
|
||||
return string.format("%02X%02X%02X", r, g, b)
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Interval system
|
||||
------------------------------------------------------------
|
||||
|
||||
local function clamp_interval_ms(value)
|
||||
return math.max(50, math.min(5000, tonumber(value) or 250))
|
||||
end
|
||||
|
||||
local function default_interval_for_mode()
|
||||
if minetest.is_singleplayer and minetest.is_singleplayer() then
|
||||
return 100
|
||||
end
|
||||
return 150
|
||||
end
|
||||
|
||||
function eye_spy.get_server_default_interval_ms()
|
||||
local raw = eye_spy.storage:get_string("server_default_interval_ms")
|
||||
if raw and raw ~= "" then
|
||||
return clamp_interval_ms(raw)
|
||||
end
|
||||
return default_interval_for_mode()
|
||||
end
|
||||
|
||||
function eye_spy.set_server_default_interval_ms(interval_ms)
|
||||
eye_spy.storage:set_string(
|
||||
"server_default_interval_ms",
|
||||
tostring(clamp_interval_ms(interval_ms))
|
||||
)
|
||||
end
|
||||
|
||||
function eye_spy.get_player_interval_override_ms(player_name)
|
||||
if not player_name or player_name == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local raw = eye_spy.storage:get_string("player_interval_ms:" .. player_name)
|
||||
if raw == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
return clamp_interval_ms(raw)
|
||||
end
|
||||
|
||||
function eye_spy.set_player_interval_override_ms(player_name, interval_ms)
|
||||
if not player_name or player_name == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local key = "player_interval_ms:" .. player_name
|
||||
|
||||
if interval_ms == nil then
|
||||
eye_spy.storage:set_string(key, "")
|
||||
return
|
||||
end
|
||||
|
||||
eye_spy.storage:set_string(key, tostring(clamp_interval_ms(interval_ms)))
|
||||
end
|
||||
|
||||
function eye_spy.get_effective_interval_ms(player_name)
|
||||
return eye_spy.get_player_interval_override_ms(player_name)
|
||||
or eye_spy.get_server_default_interval_ms()
|
||||
end
|
||||
|
||||
function eye_spy.apply_player_interval(player)
|
||||
if not player then return end
|
||||
local meta = player:get_meta()
|
||||
local player_name = player:get_player_name()
|
||||
meta:set_int("es_update_interval_ms", eye_spy.get_effective_interval_ms(player_name))
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Admin / player disable system
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Returns true if a server admin has suppressed this player's HUD.
|
||||
function eye_spy.is_player_server_disabled(player_name)
|
||||
if not player_name or player_name == "" then return false end
|
||||
return eye_spy.storage:get_string("player_disabled:" .. player_name) == "true"
|
||||
end
|
||||
|
||||
-- Sets or clears the server-level disable flag for a player.
|
||||
function eye_spy.set_player_server_disabled(player_name, disabled)
|
||||
if not player_name or player_name == "" then return end
|
||||
eye_spy.storage:set_string(
|
||||
"player_disabled:" .. player_name,
|
||||
disabled and "true" or ""
|
||||
)
|
||||
end
|
||||
|
||||
-- Returns true if the player has self-disabled their HUD via meta.
|
||||
function eye_spy.is_player_self_disabled(player_name)
|
||||
local player = minetest.get_player_by_name(player_name)
|
||||
if not player then return false end
|
||||
return player:get_meta():get_string("es_hud_enabled") == "false"
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Player state management
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Returns the runtime state table for a player, creating it if absent.
|
||||
function eye_spy.get_player_state(player)
|
||||
local player_name = player:get_player_name()
|
||||
local state = eye_spy.state.players[player_name]
|
||||
|
||||
if not state then
|
||||
state = {
|
||||
timer = 0,
|
||||
edit_mode = false,
|
||||
last_target_key = nil,
|
||||
last_render_key = nil,
|
||||
last_geom_key = nil,
|
||||
last_probe_pos = nil,
|
||||
last_probe_dir = nil,
|
||||
cached_target = nil,
|
||||
skip_static_acquire = false,
|
||||
hud_hidden_since_us = nil,
|
||||
last_recover_try_us = 0,
|
||||
hud_ids = { lines = {} },
|
||||
}
|
||||
eye_spy.state.players[player_name] = state
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
-- Clears all cached per-player data and the layout signature.
|
||||
function eye_spy.invalidate_player(player)
|
||||
local player_name = player:get_player_name()
|
||||
local state = eye_spy.get_player_state(player)
|
||||
|
||||
state.last_target_key = nil
|
||||
state.last_render_key = nil
|
||||
state.last_geom_key = nil
|
||||
state.cached_target = nil
|
||||
state.last_probe_pos = nil
|
||||
state.last_probe_dir = nil
|
||||
state.skip_static_acquire = false
|
||||
state.hud_hidden_since_us = nil
|
||||
state.last_recover_try_us = 0
|
||||
|
||||
if eye_spy.render and eye_spy.render.invalidate_layout_sig then
|
||||
eye_spy.render.invalidate_layout_sig(player_name)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Edit mode
|
||||
------------------------------------------------------------
|
||||
|
||||
function eye_spy.enter_edit_mode(player)
|
||||
if not player then return end
|
||||
|
||||
local player_name = player:get_player_name()
|
||||
local state = eye_spy.get_player_state(player)
|
||||
|
||||
if not state.edit_mode then
|
||||
eye_spy.ensure_player_defaults(player)
|
||||
state.edit_mode = true
|
||||
state.timer = 0
|
||||
eye_spy.edit_mode[player_name] = true
|
||||
|
||||
if eye_spy.ui_drafts then
|
||||
eye_spy.ui_drafts[player_name] = nil
|
||||
end
|
||||
|
||||
minetest.show_formspec(player_name, "eye_spy:ui", eye_spy.get_ui(player, { reopen = false }))
|
||||
end
|
||||
end
|
||||
|
||||
function eye_spy.exit_edit_mode(player)
|
||||
if not player then return end
|
||||
|
||||
local player_name = player:get_player_name()
|
||||
local state = eye_spy.get_player_state(player)
|
||||
|
||||
if state.edit_mode then
|
||||
local meta = player:get_meta()
|
||||
meta:set_string("es_last_obj", "")
|
||||
meta:set_int("es_last_health", -1)
|
||||
|
||||
state.edit_mode = false
|
||||
state.last_render_key = nil
|
||||
state.timer = 0
|
||||
|
||||
eye_spy.edit_mode[player_name] = false
|
||||
eye_spy.pending_preview_updates[player_name] = nil
|
||||
|
||||
if eye_spy.ui_drafts then
|
||||
eye_spy.ui_drafts[player_name] = nil
|
||||
end
|
||||
|
||||
minetest.close_formspec(player_name, "eye_spy:ui")
|
||||
minetest.close_formspec(player_name, "eye_spy:layout_ui")
|
||||
|
||||
if eye_spy.render and eye_spy.render.hide then
|
||||
eye_spy.render.hide(state, player)
|
||||
end
|
||||
|
||||
if eye_spy.render and eye_spy.render.invalidate_layout_sig then
|
||||
eye_spy.render.invalidate_layout_sig(player_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Player defaults
|
||||
------------------------------------------------------------
|
||||
|
||||
function eye_spy.ensure_player_defaults(player)
|
||||
local meta = player:get_meta()
|
||||
|
||||
if meta:get_string("es_hud_color") == "" then
|
||||
meta:set_string("es_hud_color", "26, 26, 27")
|
||||
end
|
||||
|
||||
if meta:get_string("es_hud_alignment") == "" then
|
||||
meta:set_string("es_hud_alignment", "Top-Middle")
|
||||
end
|
||||
|
||||
if meta:get_string("es_hud_health_in") == "" then
|
||||
meta:set_string("es_hud_health_in", "Points")
|
||||
end
|
||||
|
||||
if meta:get_string("es_auto_text_color") == "" then
|
||||
meta:set_string("es_auto_text_color", "true")
|
||||
end
|
||||
|
||||
if meta:get_string("es_title_color_val") == "" then
|
||||
meta:set_int("es_title_color_val", 255)
|
||||
end
|
||||
|
||||
if meta:get_string("es_subtitle_color_val") == "" then
|
||||
meta:set_int("es_subtitle_color_val", 255)
|
||||
end
|
||||
|
||||
if meta:get_string("es_footer_color_val") == "" then
|
||||
meta:set_int("es_footer_color_val", 255)
|
||||
end
|
||||
|
||||
if meta:get_string("es_line_color_val") == "" then
|
||||
meta:set_int("es_line_color_val", 255)
|
||||
end
|
||||
|
||||
if meta:get_string("es_growth_color_val") == "" then
|
||||
meta:set_int("es_growth_color_val", 110)
|
||||
end
|
||||
|
||||
if meta:get_string("es_soil_color_val") == "" then
|
||||
meta:set_int("es_soil_color_val", 165)
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_growth") == "" then
|
||||
meta:set_string("es_show_growth", eye_spy.config.show_growth and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_icons") == "" then
|
||||
meta:set_string("es_show_icons", eye_spy.config.show_icons and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_layout_target") == "" then
|
||||
meta:set_string("es_layout_target", "Global")
|
||||
end
|
||||
|
||||
if meta:get_string("es_layout_step") == "" then
|
||||
meta:set_int("es_layout_step", 2)
|
||||
end
|
||||
|
||||
if meta:get_string("es_layout_page") == "" then
|
||||
meta:set_string("es_layout_page", "Position")
|
||||
end
|
||||
|
||||
if meta:get_string("es_footer_mode") == "" then
|
||||
meta:set_string("es_footer_mode", "compact")
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_coords") == "" then
|
||||
meta:set_string("es_show_coords", eye_spy.config.default_show_coords and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_light_level") == "" then
|
||||
meta:set_string("es_show_light_level", eye_spy.config.default_show_light_level and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_spawn_hint") == "" then
|
||||
meta:set_string("es_show_spawn_hint", eye_spy.config.default_show_spawn_hint and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_liquid_info") == "" then
|
||||
meta:set_string("es_show_liquid_info", eye_spy.config.default_show_liquid_info and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_show_dig_time") == "" then
|
||||
meta:set_string("es_show_dig_time", eye_spy.config.default_show_dig_time and "true" or "false")
|
||||
end
|
||||
|
||||
if meta:get_string("es_spawn_safe_light_threshold") == "" then
|
||||
meta:set_int("es_spawn_safe_light_threshold", 8)
|
||||
end
|
||||
|
||||
-- Initialise hud-enabled flag so the disable check has a reliable baseline.
|
||||
if meta:get_string("es_hud_enabled") == "" then
|
||||
meta:set_string("es_hud_enabled", "true")
|
||||
end
|
||||
|
||||
eye_spy.apply_player_interval(player)
|
||||
|
||||
local int_defaults = {
|
||||
es_global_offset_x = 0,
|
||||
es_global_offset_y = 0,
|
||||
es_bg_offset_x = 0,
|
||||
es_bg_offset_y = 0,
|
||||
es_icon_offset_x = 0,
|
||||
es_icon_offset_y = 0,
|
||||
es_title_offset_x = 0,
|
||||
es_title_offset_y = 0,
|
||||
es_subtitle_offset_x = 0,
|
||||
es_subtitle_offset_y = 0,
|
||||
es_lines_offset_x = 0,
|
||||
es_lines_offset_y = 0,
|
||||
es_footer_offset_x = 0,
|
||||
es_footer_offset_y = 0,
|
||||
es_bg_pad_left = 7,
|
||||
es_bg_pad_right = 0,
|
||||
es_bg_pad_top = 5,
|
||||
es_bg_pad_bottom = 0,
|
||||
es_bg_extra_w = 0,
|
||||
es_bg_extra_h = 0,
|
||||
es_bg_scale_x_pct = 100,
|
||||
es_bg_scale_y_pct = 100,
|
||||
es_icon_size = 56,
|
||||
es_title_scale_pct = 100,
|
||||
es_subtitle_scale_pct = 100,
|
||||
es_line_scale_pct = 100,
|
||||
es_footer_scale_pct = 100,
|
||||
es_first_line_y_adj = 0,
|
||||
es_line_step_adj = -2, -- matches LAYOUT_META_DEFAULTS in ui.lua
|
||||
es_footer_nudge_adj = 0,
|
||||
es_text_base_x_adj = 0,
|
||||
es_icon_base_x_adj = 0,
|
||||
es_top_margin_adj = 0,
|
||||
es_bottom_margin_adj = 0,
|
||||
}
|
||||
|
||||
for key, value in pairs(int_defaults) do
|
||||
if meta:get_string(key) == "" then
|
||||
meta:set_int(key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Core update pipeline
|
||||
------------------------------------------------------------
|
||||
|
||||
local function probe_state_matches(state, player)
|
||||
if not state.last_probe_pos or not state.last_probe_dir then
|
||||
return false
|
||||
end
|
||||
|
||||
local pos = player:get_pos()
|
||||
local dir = player:get_look_dir()
|
||||
local dx = math.abs((pos.x or 0) - (state.last_probe_pos.x or 0))
|
||||
local dy = math.abs((pos.y or 0) - (state.last_probe_pos.y or 0))
|
||||
local dz = math.abs((pos.z or 0) - (state.last_probe_pos.z or 0))
|
||||
local ddx = math.abs((dir.x or 0) - (state.last_probe_dir.x or 0))
|
||||
local ddy = math.abs((dir.y or 0) - (state.last_probe_dir.y or 0))
|
||||
local ddz = math.abs((dir.z or 0) - (state.last_probe_dir.z or 0))
|
||||
|
||||
return dx < 0.05 and dy < 0.05 and dz < 0.05
|
||||
and ddx < 0.01 and ddy < 0.01 and ddz < 0.01
|
||||
end
|
||||
|
||||
local function remember_probe_state(state, player, target)
|
||||
local pos = player:get_pos()
|
||||
local dir = player:get_look_dir()
|
||||
state.last_probe_pos = vector.new(pos)
|
||||
state.last_probe_dir = vector.new(dir)
|
||||
state.cached_target = target
|
||||
end
|
||||
|
||||
function eye_spy.update_player(player, opts)
|
||||
if not player or not eye_spy.target or not eye_spy.render then
|
||||
return
|
||||
end
|
||||
|
||||
local perf_on = eye_spy.perf.enabled and minetest.get_us_time
|
||||
local update_start_us = perf_on and minetest.get_us_time() or 0
|
||||
local options = opts or {}
|
||||
local state = eye_spy.get_player_state(player)
|
||||
local target
|
||||
|
||||
if not options.force
|
||||
and not options.preview
|
||||
and state.cached_target
|
||||
and probe_state_matches(state, player)
|
||||
and state.skip_static_acquire
|
||||
then
|
||||
target = state.cached_target
|
||||
state.skip_static_acquire = false
|
||||
else
|
||||
local acquire_start_us = perf_on and minetest.get_us_time() or 0
|
||||
target = eye_spy.target.acquire(player, options)
|
||||
if perf_on then
|
||||
eye_spy.perf_record("target_acquire", minetest.get_us_time() - acquire_start_us)
|
||||
end
|
||||
|
||||
if not options.preview then
|
||||
remember_probe_state(state, player, target)
|
||||
state.skip_static_acquire = target and target.kind ~= "air"
|
||||
end
|
||||
end
|
||||
|
||||
if not target or target.kind == "air" then
|
||||
local now_us = minetest.get_us_time and minetest.get_us_time() or 0
|
||||
|
||||
eye_spy.render.hide(state, player)
|
||||
state.last_target_key = nil
|
||||
state.last_render_key = nil
|
||||
state.cached_target = nil
|
||||
state.skip_static_acquire = false
|
||||
|
||||
if now_us > 0 and not state.hud_hidden_since_us then
|
||||
state.hud_hidden_since_us = now_us
|
||||
end
|
||||
|
||||
if perf_on then
|
||||
eye_spy.perf_record("update_player_total", minetest.get_us_time() - update_start_us)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local build_start_us = perf_on and minetest.get_us_time() or 0
|
||||
local view_model = eye_spy.render.build_view_model(player, target, options)
|
||||
if perf_on then
|
||||
eye_spy.perf_record("build_view_model", minetest.get_us_time() - build_start_us)
|
||||
end
|
||||
|
||||
if not options.force and view_model.key == state.last_render_key then
|
||||
state.last_target_key = target.key
|
||||
eye_spy.perf_inc_counter("render_skip_same_key")
|
||||
if perf_on then
|
||||
eye_spy.perf_record("update_player_total", minetest.get_us_time() - update_start_us)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local render_start_us = perf_on and minetest.get_us_time() or 0
|
||||
eye_spy.render.apply(state, player, view_model)
|
||||
if perf_on then
|
||||
eye_spy.perf_record("render_apply", minetest.get_us_time() - render_start_us)
|
||||
end
|
||||
|
||||
state.last_target_key = target.key
|
||||
state.last_render_key = view_model.key
|
||||
state.hud_hidden_since_us = nil
|
||||
state.last_recover_try_us = 0
|
||||
|
||||
if perf_on then
|
||||
eye_spy.perf_record("update_player_total", minetest.get_us_time() - update_start_us)
|
||||
end
|
||||
end
|
||||
|
||||
-- Thin wrapper used by the UI and preview system.
|
||||
function eye_spy.get_hud(player, data)
|
||||
eye_spy.update_player(player, data or {})
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Main step function
|
||||
------------------------------------------------------------
|
||||
|
||||
function eye_spy.step(dtime)
|
||||
local perf_on = eye_spy.perf.enabled and minetest.get_us_time
|
||||
local step_start_us = perf_on and minetest.get_us_time() or 0
|
||||
local now_us = minetest.get_us_time and minetest.get_us_time() or 0
|
||||
|
||||
for _, player in ipairs(minetest.get_connected_players()) do
|
||||
local state = eye_spy.get_player_state(player)
|
||||
local player_name = player:get_player_name()
|
||||
local pending_preview = eye_spy.pending_preview_updates[player_name]
|
||||
|
||||
-- Service pending edit-mode preview updates.
|
||||
if state.edit_mode and pending_preview then
|
||||
local due_at = pending_preview.due_at_us or 0
|
||||
|
||||
if pending_preview.force or due_at <= now_us then
|
||||
eye_spy.pending_preview_updates[player_name] = nil
|
||||
eye_spy.update_player(player, {
|
||||
preview = true,
|
||||
preview_type = pending_preview.preview_type,
|
||||
force = pending_preview.force == true,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
-- Service pending layout-UI refreshes.
|
||||
local pending_layout_refresh = eye_spy.pending_layout_refreshes[player_name]
|
||||
if state.edit_mode
|
||||
and pending_layout_refresh
|
||||
and pending_layout_refresh.due_at_us <= now_us
|
||||
then
|
||||
eye_spy.pending_layout_refreshes[player_name] = nil
|
||||
minetest.show_formspec(player_name, "eye_spy:layout_ui", eye_spy.get_layout_ui(player))
|
||||
end
|
||||
|
||||
if not state.edit_mode then
|
||||
local meta = player:get_meta()
|
||||
|
||||
-- Check both server-admin and player-self disable flags.
|
||||
local hud_disabled = eye_spy.is_player_server_disabled(player_name)
|
||||
or (meta:get_string("es_hud_enabled") == "false")
|
||||
|
||||
if hud_disabled then
|
||||
if state.last_render_key then
|
||||
eye_spy.render.hide(state, player)
|
||||
state.last_target_key = nil
|
||||
state.last_render_key = nil
|
||||
state.cached_target = nil
|
||||
state.skip_static_acquire = false
|
||||
end
|
||||
state.timer = 0
|
||||
goto continue_player
|
||||
end
|
||||
|
||||
local interval_ms = meta:get_int("es_update_interval_ms")
|
||||
local interval = eye_spy.get_server_default_interval_ms() / 1000
|
||||
|
||||
if interval_ms > 0 then
|
||||
interval = interval_ms / 1000
|
||||
end
|
||||
|
||||
interval = math.max(0.05, math.min(5, interval))
|
||||
state.timer = state.timer + math.min(dtime, interval)
|
||||
|
||||
if state.timer >= interval then
|
||||
state.timer = 0
|
||||
eye_spy.update_player(player, { preview = false })
|
||||
end
|
||||
|
||||
-- Focus-loss / self-heal: if the HUD appears stuck hidden while we still
|
||||
-- have a valid cached target, force an occasional refresh attempt.
|
||||
if state.hud_hidden_since_us
|
||||
and state.cached_target
|
||||
and state.cached_target.kind ~= "air"
|
||||
then
|
||||
local hidden_for_us = now_us - state.hud_hidden_since_us
|
||||
local recover_cooldown_us = 900000
|
||||
|
||||
if hidden_for_us >= 1200000
|
||||
and (state.last_recover_try_us or 0) + recover_cooldown_us <= now_us
|
||||
then
|
||||
state.last_recover_try_us = now_us
|
||||
eye_spy.perf_inc_counter("hud_recover_forced_refresh")
|
||||
eye_spy.update_player(player, { preview = false, force = true })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
::continue_player::
|
||||
end
|
||||
|
||||
if perf_on then
|
||||
eye_spy.perf_record("step_total", minetest.get_us_time() - step_start_us)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- HUD cleanup
|
||||
------------------------------------------------------------
|
||||
|
||||
function eye_spy.remove_huds(player)
|
||||
if eye_spy.render then eye_spy.render.remove(player) end
|
||||
end
|
||||
@@ -0,0 +1,97 @@
|
||||
--init
|
||||
|
||||
local modpath = minetest.get_modpath("eye_spy")
|
||||
|
||||
-- Load modules in dependency order:
|
||||
-- 1. Core declarations + helpers (defines eye_spy = {})
|
||||
dofile(modpath .. "/functions.lua")
|
||||
|
||||
-- 2. Config (needs eye_spy.storage from functions.lua)
|
||||
dofile(modpath .. "/config.lua")
|
||||
|
||||
-- 3. Performance metrics (needs eye_spy.config)
|
||||
dofile(modpath .. "/perf.lua")
|
||||
|
||||
-- 4. Pure utilities (no minetest runtime deps)
|
||||
dofile(modpath .. "/colors.lua")
|
||||
dofile(modpath .. "/tools.lua") -- needs eye_spy.S
|
||||
|
||||
-- 5. Target acquisition + enrichment
|
||||
dofile(modpath .. "/target.lua")
|
||||
dofile(modpath .. "/enrichers.lua")
|
||||
|
||||
-- 6. Rendering
|
||||
dofile(modpath .. "/render.lua")
|
||||
|
||||
-- 7. Registration (entities, nodes)
|
||||
dofile(modpath .. "/preview.lua")
|
||||
|
||||
-- 8. UI + Commands (last, can reference everything above)
|
||||
dofile(modpath .. "/ui.lua")
|
||||
dofile(modpath .. "/commands.lua")
|
||||
|
||||
-- Detect game type — sets eye_spy.cg used by tools.lua at runtime.
|
||||
-- tools.lua reads eye_spy.cg inside function bodies, not at load time, so this is fine.
|
||||
if minetest.get_modpath("default") then
|
||||
eye_spy.cg = "minetest_game"
|
||||
elseif minetest.get_modpath("mcl_core") then
|
||||
eye_spy.cg = "mineclone"
|
||||
-- Note: voxelibre/mineclonia distinction reserved for future differentiation.
|
||||
if minetest.get_modpath("vl_legacy") then
|
||||
eye_spy.cgs = "voxelibre"
|
||||
else
|
||||
eye_spy.cgs = "mineclonia"
|
||||
end
|
||||
end
|
||||
|
||||
-- Player join: initialise per-player state and defaults.
|
||||
minetest.register_on_joinplayer(function(player)
|
||||
local meta = player:get_meta()
|
||||
|
||||
-- Clear legacy tracking keys.
|
||||
meta:set_string("es_last_obj", "")
|
||||
meta:set_int("es_last_health", -1)
|
||||
|
||||
-- Apply defaults for any keys not yet set.
|
||||
eye_spy.ensure_player_defaults(player)
|
||||
|
||||
-- Initialise runtime state.
|
||||
local state = eye_spy.get_player_state(player)
|
||||
state.timer = 0
|
||||
state.edit_mode = false
|
||||
|
||||
eye_spy.edit_mode[player:get_player_name()] = false
|
||||
end)
|
||||
|
||||
-- Player leave: clean up ALL per-player runtime data.
|
||||
minetest.register_on_leaveplayer(function(player)
|
||||
local player_name = player:get_player_name()
|
||||
|
||||
-- Runtime state tables.
|
||||
eye_spy.state.players[player_name] = nil
|
||||
eye_spy.edit_mode[player_name] = nil
|
||||
eye_spy.pending_preview_updates[player_name] = nil
|
||||
eye_spy.pending_layout_refreshes[player_name] = nil
|
||||
eye_spy.ui_drafts[player_name] = nil
|
||||
|
||||
-- Invalidate render layout signature cache.
|
||||
if eye_spy.render and eye_spy.render.invalidate_layout_sig then
|
||||
eye_spy.render.invalidate_layout_sig(player_name)
|
||||
end
|
||||
|
||||
-- Enricher tool-hint cache.
|
||||
if eye_spy.enrichers and eye_spy.enrichers.on_player_leave then
|
||||
eye_spy.enrichers.on_player_leave(player_name)
|
||||
end
|
||||
|
||||
-- HUD elements are automatically destroyed by the engine when a player leaves,
|
||||
-- but we must remove our engine references to prevent stale ID usage on rejoin.
|
||||
eye_spy.remove_huds(player)
|
||||
end)
|
||||
|
||||
-- Main game loop.
|
||||
minetest.register_globalstep(function(dtime)
|
||||
eye_spy.step(dtime)
|
||||
end)
|
||||
|
||||
minetest.log("action", "[eye_spy] Mod loaded successfully.")
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Ruka
|
||||
Pickaxe=Krápáč / Krumpáč
|
||||
Sword=Meč
|
||||
Shears=Nůžky
|
||||
Axe=Sekera
|
||||
Shovel=Lopata
|
||||
|
||||
Unbreakable=Nezničitelné
|
||||
None=Žádné
|
||||
Type=Typ
|
||||
Health=Zdraví
|
||||
|
||||
Unknown=Neznámé
|
||||
|
||||
Preview=Náhled
|
||||
Hud Alignment=Zarovnání HUD
|
||||
Top-Middle=Nahoře-Střed
|
||||
Top-Right=Nahoře-Vpravo
|
||||
Top-Left=Nahoře-Vlevo
|
||||
Middle-Right=Střed-Vpravo
|
||||
Middle-Left=Střed-Vlevo
|
||||
Bottom-Right=Dole-Vpravo
|
||||
Bottom-Left=Dole-Vlevo
|
||||
Health in=Zdraví v
|
||||
Points=Body
|
||||
Hearts=Srdce
|
||||
Set to Default=Obnovit výchozí
|
||||
Change Preview to=Změnit náhled na
|
||||
Node=Uzel
|
||||
Entity=Entita
|
||||
Cancel=Zrušit
|
||||
Apply Changes=Použít změny
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Hand
|
||||
Pickaxe=Spitzhacke
|
||||
Sword=Schwert
|
||||
Shears=Schere
|
||||
Axe=Axt
|
||||
Shovel=Schaufel
|
||||
|
||||
Unbreakable=Unzerstörbar
|
||||
None=Keiner
|
||||
Type=Typ
|
||||
Health=Gesundheit
|
||||
|
||||
Unknown=Unbekannt
|
||||
|
||||
Preview=Vorschau
|
||||
Hud Alignment=Hudausrichtung
|
||||
Top-Middle=Oben-Mitte
|
||||
Top-Right=Oben-Rechts
|
||||
Top-Left=Oben-Links
|
||||
Middle-Right=Mitte-Rechts
|
||||
Middle-Left=Mitte-Links
|
||||
Bottom-Right=Unten-Rechts
|
||||
Bottom-Left=Unten-Links
|
||||
Health in=Gesundheit in
|
||||
Points=Punkten
|
||||
Hearts=Herzen
|
||||
Set to Default=auf Standart setzen
|
||||
Change Preview to=Vorschau ändern zu
|
||||
Node=Node
|
||||
Entity=Entity
|
||||
Cancel=Abbrechen
|
||||
Apply Changes=Änderungen übernehmen
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Mano
|
||||
Pickaxe=Pico
|
||||
Sword=Espada
|
||||
Shears=Tijeras
|
||||
Axe=Hacha
|
||||
Shovel=Pala
|
||||
|
||||
Unbreakable=Irrompible
|
||||
None=Ninguno
|
||||
Type=Tipo
|
||||
Health=Salud
|
||||
|
||||
Unknown=Desconocido
|
||||
|
||||
Preview=Vista previa
|
||||
Hud Alignment=Alineación del HUD
|
||||
Top-Middle=Arriba-Centro
|
||||
Top-Right=Arriba-Derecha
|
||||
Top-Left=Arriba-Izquierda
|
||||
Middle-Right=Centro-Derecha
|
||||
Middle-Left=Centro-Izquierda
|
||||
Bottom-Right=Abajo-Derecha
|
||||
Bottom-Left=Abajo-Izquierda
|
||||
Health in=Salud en
|
||||
Points=Puntos
|
||||
Hearts=Corazones
|
||||
Set to Default=Restablecer valores
|
||||
Change Preview to=Cambiar vista previa a
|
||||
Node=Nodo
|
||||
Entity=Entidad
|
||||
Cancel=Cancelar
|
||||
Apply Changes=Aplicar cambios
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Main
|
||||
Pickaxe=Pioche
|
||||
Sword=Épée
|
||||
Shears=Cisailles
|
||||
Axe=Hache
|
||||
Shovel=Pelle
|
||||
|
||||
Unbreakable=Incassable
|
||||
None=Aucun
|
||||
Type=Type
|
||||
Health=Santé
|
||||
|
||||
Unknown=Inconnu
|
||||
|
||||
Preview=Aperçu
|
||||
Hud Alignment=Alignement HUD
|
||||
Top-Middle=Haut-Centre
|
||||
Top-Right=Haut-Droite
|
||||
Top-Left=Haut-Gauche
|
||||
Middle-Right=Milieu-Droite
|
||||
Middle-Left=Milieu-Gauche
|
||||
Bottom-Right=Bas-Droite
|
||||
Bottom-Left=Bas-Gauche
|
||||
Health in=Vie en
|
||||
Points=Points
|
||||
Hearts=Cœurs
|
||||
Set to Default=Réinitialiser
|
||||
Change Preview to=Changer l’aperçu en
|
||||
Node=Nœud
|
||||
Entity=Entité
|
||||
Cancel=Annuler
|
||||
Apply Changes=Appliquer les modifications
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Mano
|
||||
Pickaxe=Piccone
|
||||
Sword=Spada
|
||||
Shears=Cesoie
|
||||
Axe=Ascia
|
||||
Shovel=Pala
|
||||
|
||||
Unbreakable=Indistruttibile
|
||||
None=Nessuno
|
||||
Type=Tipo
|
||||
Health=Salute
|
||||
|
||||
Unknown=Sconosciuto
|
||||
|
||||
Preview=Anteprima
|
||||
Hud Alignment=Allineamento HUD
|
||||
Top-Middle=Alto-Centro
|
||||
Top-Right=Alto-Destra
|
||||
Top-Left=Alto-Sinistra
|
||||
Middle-Right=Centro-Destra
|
||||
Middle-Left=Centro-Sinistra
|
||||
Bottom-Right=Basso-Destra
|
||||
Bottom-Left=Basso-Sinistra
|
||||
Health in=Salute in
|
||||
Points=Punti
|
||||
Hearts=Cuori
|
||||
Set to Default=Ripristina predefiniti
|
||||
Change Preview to=Cambia anteprima in
|
||||
Node=Nodo
|
||||
Entity=Entità
|
||||
Cancel=Annulla
|
||||
Apply Changes=Applica modifiche
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Hand
|
||||
Pickaxe=Houwelen / Pikhouweel
|
||||
Sword=Zwaard
|
||||
Shears=Snoeischaar
|
||||
Axe=Bijl
|
||||
Shovel=Schop
|
||||
|
||||
Unbreakable=Onbreekbaar
|
||||
None=Geen
|
||||
Type=Type
|
||||
Health=Gezondheid
|
||||
|
||||
Unknown=Onbekend
|
||||
|
||||
Preview=Voorbeeld
|
||||
Hud Alignment=HUD-uitlijning
|
||||
Top-Middle=Boven-Midden
|
||||
Top-Right=Boven-Rechts
|
||||
Top-Left=Boven-Links
|
||||
Middle-Right=Midden-Rechts
|
||||
Middle-Left=Midden-Links
|
||||
Bottom-Right=Onder-Rechts
|
||||
Bottom-Left=Onder-Links
|
||||
Health in=Gezondheid in
|
||||
Points=Punten
|
||||
Hearts=Harten
|
||||
Set to Default=Standaard herstellen
|
||||
Change Preview to=Voorbeeld wijzigen naar
|
||||
Node=Node
|
||||
Entity=Entiteit
|
||||
Cancel=Annuleren
|
||||
Apply Changes=Wijzigingen toepassen
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Ręka
|
||||
Pickaxe=Kilof
|
||||
Sword=Miecz
|
||||
Shears=Nożyce
|
||||
Axe=Siekiera
|
||||
Shovel=Łopata
|
||||
|
||||
Unbreakable=Niezniszczalny
|
||||
None=Brak
|
||||
Type=Typ
|
||||
Health=Zdrowie
|
||||
|
||||
Unknown=Nieznane
|
||||
|
||||
Preview=Podgląd
|
||||
Hud Alignment=Wyrównanie HUD
|
||||
Top-Middle=Góra-Środek
|
||||
Top-Right=Góra-Prawo
|
||||
Top-Left=Góra-Lewo
|
||||
Middle-Right=Środek-Prawo
|
||||
Middle-Left=Środek-Lewo
|
||||
Bottom-Right=Dół-Prawo
|
||||
Bottom-Left=Dół-Lewo
|
||||
Health in=Zdrowie w
|
||||
Points=Punkty
|
||||
Hearts=Serca
|
||||
Set to Default=Przywróć domyślne
|
||||
Change Preview to=Zmień podgląd na
|
||||
Node=Węzeł
|
||||
Entity=Encja
|
||||
Cancel=Anuluj
|
||||
Apply Changes=Zastosuj zmiany
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,38 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
Hand=Рука
|
||||
Pickaxe=Кирка
|
||||
Sword=Меч
|
||||
Shears=Ножницы
|
||||
Axe=Топор
|
||||
Shovel=Лопата
|
||||
|
||||
Unbreakable=Неразрушимый
|
||||
None=Нет
|
||||
Type=Тип
|
||||
Health=Здоровье
|
||||
|
||||
Unknown=Неизвестно
|
||||
|
||||
Preview=Предпросмотр
|
||||
Hud Alignment=Выравнивание HUD
|
||||
Top-Middle=Верх-Середина
|
||||
Top-Right=Верх-Право
|
||||
Top-Left=Верх-Лево
|
||||
Middle-Right=Середина-Право
|
||||
Middle-Left=Середина-Лево
|
||||
Bottom-Right=Низ-Право
|
||||
Bottom-Left=Низ-Лево
|
||||
Health in=Здоровье в
|
||||
Points=Очки
|
||||
Hearts=Сердца
|
||||
Set to Default=Сбросить по умолчанию
|
||||
Change Preview to=Изменить предпросмотр на
|
||||
Node=Блок
|
||||
Entity=Сущность
|
||||
Cancel=Отмена
|
||||
Apply Changes=Применить изменения
|
||||
|
||||
Count=
|
||||
Durability=
|
||||
Metadata=
|
||||
@@ -0,0 +1,91 @@
|
||||
# textdomain: eye_spy
|
||||
|
||||
+%d more=
|
||||
Advanced Layout=
|
||||
Advanced Layout Editor=
|
||||
Apply Changes=
|
||||
Auto Text Colors=
|
||||
Axe=
|
||||
Back=
|
||||
Background Color=
|
||||
Bad Light=
|
||||
Bad Soil=
|
||||
Cancel=
|
||||
Coords=
|
||||
Count=
|
||||
Dark Enough=
|
||||
Default=
|
||||
Dig Time=
|
||||
Dry=
|
||||
Dry Soil=
|
||||
Durability=
|
||||
Entity=
|
||||
Est=
|
||||
Flowing=
|
||||
Footer=
|
||||
Footer Mode=
|
||||
Growth=
|
||||
HUD Setup=
|
||||
Hand=
|
||||
Health=
|
||||
Health in=
|
||||
Hud Alignment=
|
||||
Icons=
|
||||
Info=
|
||||
Info Lines=
|
||||
Item=
|
||||
Lava=
|
||||
Level=
|
||||
Light=
|
||||
Light Level=
|
||||
Likely Safe=
|
||||
Line=
|
||||
Liquid=
|
||||
Liquid Info=
|
||||
Maturity=
|
||||
Metadata=
|
||||
Middle-Left=
|
||||
Middle-Right=
|
||||
No Soil=
|
||||
No controls for this target on this page=
|
||||
Node=
|
||||
None=
|
||||
Pickaxe=
|
||||
Points=
|
||||
Preview=
|
||||
Preview Sample=
|
||||
Preview Type=
|
||||
Reset All=
|
||||
Reset Target=
|
||||
Rooting=
|
||||
Selected Target Position=
|
||||
Set to Default=
|
||||
Shears=
|
||||
Shovel=
|
||||
Soil=
|
||||
Source=
|
||||
Spawn Hint=
|
||||
Spawn Risk=
|
||||
Spawn Thresh=
|
||||
Stalled=
|
||||
Status=
|
||||
Step=
|
||||
Suitable=
|
||||
Sword=
|
||||
Text Styling=
|
||||
Tips: use step 1 for fine tuning and drag sliders for live preview=
|
||||
Title=
|
||||
Top-Left=
|
||||
Top-Middle=
|
||||
Top-Right=
|
||||
Unbreakable=
|
||||
Type=
|
||||
Unknown=
|
||||
Bottom-Right=
|
||||
Bottom-Left=
|
||||
Hearts=
|
||||
Change Preview to=
|
||||
Unsuitable=
|
||||
Use pages to edit layout without crowding the form=
|
||||
Water=
|
||||
Wet=
|
||||
@@ -0,0 +1,7 @@
|
||||
name = eye_spy
|
||||
title = Eye Spy
|
||||
description = Lets you see information about the node/entity/item you are looking at.
|
||||
optional_depends = default, mcl_core, mcl_tools, mcl_doc
|
||||
supported_games = minetest_game, mineclone2, mineclonia
|
||||
min_minetest_version = 5.8.0
|
||||
author = cjgray24
|
||||
@@ -0,0 +1,223 @@
|
||||
-- eye_spy/perf.lua
|
||||
-- Performance metrics collection module for the eye_spy mod.
|
||||
-- Extracted from functions.lua; loaded after functions.lua and config.lua.
|
||||
--
|
||||
-- Exposes:
|
||||
-- eye_spy.perf (table) live metrics state
|
||||
-- eye_spy.perf_set_enabled(b) enable/disable + persist to storage
|
||||
-- eye_spy.perf_reset() clear all samples and counters
|
||||
-- eye_spy.perf_inc_counter(k,d) increment a named event counter
|
||||
-- eye_spy.perf_record(k,us) record a timed sample in microseconds
|
||||
-- eye_spy.perf_report() return a formatted report string
|
||||
--
|
||||
-- All public functions are no-ops when eye_spy.perf.enabled == false.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Internal constants
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Maximum number of rolling samples kept per metric.
|
||||
-- Older samples are overwritten once the ring buffer is full.
|
||||
local PERF_SAMPLE_CAP = 200
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Initialise eye_spy.perf state
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- eye_spy and eye_spy.config are guaranteed to exist at this point.
|
||||
-- Restore the persisted enabled-flag from mod storage if present;
|
||||
-- otherwise fall back to the minetest.conf value read in config.lua.
|
||||
|
||||
do
|
||||
local persisted = eye_spy.storage:get_string("perf_metrics_enabled")
|
||||
local enabled
|
||||
|
||||
if persisted == "true" then
|
||||
enabled = true
|
||||
elseif persisted == "false" then
|
||||
enabled = false
|
||||
else
|
||||
-- No persisted override — use the value from minetest.conf.
|
||||
enabled = eye_spy.config.perf_metrics_enabled
|
||||
end
|
||||
|
||||
-- Preserve any table that was already created (e.g. by a /lua reload).
|
||||
eye_spy.perf = eye_spy.perf or {}
|
||||
eye_spy.perf.enabled = enabled
|
||||
eye_spy.perf.metrics = eye_spy.perf.metrics or {}
|
||||
eye_spy.perf.counters = eye_spy.perf.counters or {}
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Local helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Compute the 95th-percentile sample value for a metric.
|
||||
-- Uses only the samples that have actually been filled so far.
|
||||
-- @param metric A metric entry from eye_spy.perf.metrics.
|
||||
-- @return The p95 value in microseconds, or 0 if no samples.
|
||||
local function metric_p95(metric)
|
||||
local n = metric and metric.sample_filled or 0
|
||||
|
||||
if n <= 0 then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Copy filled samples into a temporary table for sorting.
|
||||
local values = {}
|
||||
for i = 1, n do
|
||||
values[i] = metric.samples[i] or 0
|
||||
end
|
||||
|
||||
table.sort(values)
|
||||
|
||||
local idx = math.max(1, math.ceil(n * 0.95))
|
||||
return values[idx] or 0
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Public API
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Enable or disable performance metrics collection and persist the choice.
|
||||
-- When disabled all other perf functions become no-ops on the next call.
|
||||
-- @param enabled boolean — true to enable, false/nil to disable.
|
||||
function eye_spy.perf_set_enabled(enabled)
|
||||
local value = (enabled == true)
|
||||
eye_spy.perf.enabled = value
|
||||
eye_spy.storage:set_string("perf_metrics_enabled", value and "true" or "false")
|
||||
end
|
||||
|
||||
--- Reset all collected metrics and event counters.
|
||||
-- Useful before starting a fresh measurement session.
|
||||
function eye_spy.perf_reset()
|
||||
eye_spy.perf.metrics = {}
|
||||
eye_spy.perf.counters = {}
|
||||
end
|
||||
|
||||
--- Increment a named event counter by delta (default 1).
|
||||
-- Silently ignored when metrics are disabled or the key is empty.
|
||||
-- @param counter_name string Name of the counter to increment.
|
||||
-- @param delta number Amount to add (default 1; may be negative).
|
||||
function eye_spy.perf_inc_counter(counter_name, delta)
|
||||
if not eye_spy.perf.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
local key = tostring(counter_name or "")
|
||||
if key == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local step = tonumber(delta) or 1
|
||||
if step == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local counters = eye_spy.perf.counters
|
||||
counters[key] = (counters[key] or 0) + step
|
||||
end
|
||||
|
||||
--- Record a timing sample for a named metric.
|
||||
-- Samples are stored in a fixed-size ring buffer (PERF_SAMPLE_CAP entries).
|
||||
-- Silently ignored when metrics are disabled or duration is negative.
|
||||
-- @param metric_name string Name of the metric (e.g. "update_player").
|
||||
-- @param duration_us number Elapsed time for this sample, in microseconds.
|
||||
function eye_spy.perf_record(metric_name, duration_us)
|
||||
if not eye_spy.perf.enabled then
|
||||
return
|
||||
end
|
||||
|
||||
local us = tonumber(duration_us) or 0
|
||||
if us < 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local metrics = eye_spy.perf.metrics
|
||||
local metric = metrics[metric_name]
|
||||
|
||||
-- Lazily create the metric entry on first use.
|
||||
if not metric then
|
||||
metric = {
|
||||
count = 0, -- total number of samples recorded
|
||||
total_us = 0, -- cumulative sum of all sample durations
|
||||
max_us = 0, -- single worst-case duration seen
|
||||
samples = {}, -- ring buffer of recent raw durations
|
||||
sample_index = 1, -- write head for the ring buffer (1-based)
|
||||
sample_filled = 0, -- how many slots are populated (<= PERF_SAMPLE_CAP)
|
||||
}
|
||||
metrics[metric_name] = metric
|
||||
end
|
||||
|
||||
-- Update aggregate statistics.
|
||||
metric.count = metric.count + 1
|
||||
metric.total_us = metric.total_us + us
|
||||
metric.max_us = math.max(metric.max_us, us)
|
||||
|
||||
-- Write into ring buffer and advance the write head.
|
||||
metric.samples[metric.sample_index] = us
|
||||
metric.sample_index = metric.sample_index + 1
|
||||
|
||||
if metric.sample_index > PERF_SAMPLE_CAP then
|
||||
metric.sample_index = 1
|
||||
end
|
||||
|
||||
if metric.sample_filled < PERF_SAMPLE_CAP then
|
||||
metric.sample_filled = metric.sample_filled + 1
|
||||
end
|
||||
end
|
||||
|
||||
--- Build and return a human-readable performance report string.
|
||||
-- Lists every metric that has at least one sample, sorted alphabetically,
|
||||
-- followed by every non-zero event counter.
|
||||
-- Returns a short placeholder message if no data has been collected yet.
|
||||
-- @return string Multi-line report.
|
||||
function eye_spy.perf_report()
|
||||
-- Collect metric keys that have real data.
|
||||
local keys = {}
|
||||
for key, metric in pairs(eye_spy.perf.metrics) do
|
||||
if metric and metric.count > 0 then
|
||||
keys[#keys + 1] = key
|
||||
end
|
||||
end
|
||||
table.sort(keys)
|
||||
|
||||
-- Collect counter keys that are non-zero.
|
||||
local counter_keys = {}
|
||||
for key, value in pairs(eye_spy.perf.counters or {}) do
|
||||
if (tonumber(value) or 0) ~= 0 then
|
||||
counter_keys[#counter_keys + 1] = key
|
||||
end
|
||||
end
|
||||
table.sort(counter_keys)
|
||||
|
||||
if #keys == 0 and #counter_keys == 0 then
|
||||
return "No Eye Spy perf samples yet"
|
||||
end
|
||||
|
||||
local lines = { "Eye Spy perf (avg/p95/max in ms):" }
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
local metric = eye_spy.perf.metrics[key]
|
||||
local avg_ms = (metric.total_us / math.max(metric.count, 1)) / 1000
|
||||
local p95_ms = metric_p95(metric) / 1000
|
||||
local max_ms = metric.max_us / 1000
|
||||
|
||||
lines[#lines + 1] = string.format(
|
||||
"%s: avg=%.3f p95=%.3f max=%.3f n=%d",
|
||||
key, avg_ms, p95_ms, max_ms, metric.count
|
||||
)
|
||||
end
|
||||
|
||||
if #counter_keys > 0 then
|
||||
lines[#lines + 1] = "Eye Spy perf counters:"
|
||||
|
||||
for _, key in ipairs(counter_keys) do
|
||||
lines[#lines + 1] = string.format(
|
||||
"%s: n=%d",
|
||||
key, eye_spy.perf.counters[key] or 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
--preview
|
||||
|
||||
local S = minetest.get_translator(minetest.get_current_modname())
|
||||
|
||||
local preview_texture = "eye_spy_preview_texture2.png"
|
||||
|
||||
minetest.register_node("eye_spy:preview_node", {
|
||||
description = S("Preview") .. " " .. S("Node"),
|
||||
tiles = { preview_texture },
|
||||
groups = { cracky = 3, level = 1, not_in_creative_inventory = 1 },
|
||||
drop = "",
|
||||
})
|
||||
|
||||
minetest.register_entity("eye_spy:preview_entity", {
|
||||
initial_properties = {
|
||||
description = S("Preview") .. " " .. S("Entity"),
|
||||
physical = false,
|
||||
collide_with_objects = false,
|
||||
collisionbox = { -0.0, -0.0, -0.0, 0.0, 0.0, 0.0 },
|
||||
visual = "cube",
|
||||
textures = {
|
||||
preview_texture, preview_texture, preview_texture,
|
||||
preview_texture, preview_texture, preview_texture,
|
||||
},
|
||||
health = 8,
|
||||
max_hp = 10,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,864 @@
|
||||
local S = eye_spy.S
|
||||
|
||||
eye_spy.render = {}
|
||||
-- Bump this string whenever the HUD layout math, element set, or render key
|
||||
-- format changes in a way that would make old cached render keys produce a
|
||||
-- wrong or stale layout for existing players.
|
||||
local RENDER_LAYOUT_REV = "advanced-layout-controls-v2-padding-fix"
|
||||
local ICON_TEXTURE_SIZE = 32
|
||||
local ICON_DISPLAY_SIZE = 56
|
||||
local ENTITY_ICON_TEXTURE = "heart.png"
|
||||
local BASE_LAYOUT_LINE_ROWS = 6
|
||||
local MAX_LAYOUT_SCALE_FACTOR = 3
|
||||
|
||||
local FIXED_LINE_ROW_BY_ID = {
|
||||
dig_time = 1,
|
||||
light = 2,
|
||||
soil = 3,
|
||||
growth = 4,
|
||||
spawn = 5,
|
||||
liquid = 6,
|
||||
}
|
||||
|
||||
local FIXED_LINE_ORDER = {
|
||||
"dig_time",
|
||||
"light",
|
||||
"soil",
|
||||
"growth",
|
||||
"spawn",
|
||||
"liquid",
|
||||
}
|
||||
|
||||
local layout_key_names = {
|
||||
"es_global_offset_x",
|
||||
"es_global_offset_y",
|
||||
"es_bg_offset_x",
|
||||
"es_bg_offset_y",
|
||||
"es_icon_offset_x",
|
||||
"es_icon_offset_y",
|
||||
"es_title_offset_x",
|
||||
"es_title_offset_y",
|
||||
"es_subtitle_offset_x",
|
||||
"es_subtitle_offset_y",
|
||||
"es_lines_offset_x",
|
||||
"es_lines_offset_y",
|
||||
"es_footer_offset_x",
|
||||
"es_footer_offset_y",
|
||||
"es_bg_pad_left",
|
||||
"es_bg_pad_right",
|
||||
"es_bg_pad_top",
|
||||
"es_bg_pad_bottom",
|
||||
"es_bg_extra_w",
|
||||
"es_bg_extra_h",
|
||||
"es_bg_scale_x_pct",
|
||||
"es_bg_scale_y_pct",
|
||||
"es_icon_size",
|
||||
"es_title_scale_pct",
|
||||
"es_subtitle_scale_pct",
|
||||
"es_line_scale_pct",
|
||||
"es_footer_scale_pct",
|
||||
"es_first_line_y_adj",
|
||||
"es_line_step_adj",
|
||||
"es_footer_nudge_adj",
|
||||
"es_text_base_x_adj",
|
||||
"es_icon_base_x_adj",
|
||||
"es_top_margin_adj",
|
||||
"es_bottom_margin_adj",
|
||||
}
|
||||
|
||||
local layouts = {
|
||||
["Top-Middle"] = { pos = { x = 0.5, y = 0 }, x_mode = "center", y_mode = "top" },
|
||||
["Top-Right"] = { pos = { x = 1, y = 0 }, x_mode = "right", y_mode = "top" },
|
||||
["Top-Left"] = { pos = { x = 0, y = 0 }, x_mode = "left", y_mode = "top" },
|
||||
["Middle-Right"] = { pos = { x = 1, y = 0.5 }, x_mode = "right", y_mode = "middle" },
|
||||
["Middle-Left"] = { pos = { x = 0, y = 0.5 }, x_mode = "left", y_mode = "middle" },
|
||||
["Bottom-Right"] = { pos = { x = 1, y = 1 }, x_mode = "right", y_mode = "bottom" },
|
||||
["Bottom-Left"] = { pos = { x = 0, y = 1 }, x_mode = "left", y_mode = "bottom" },
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Color math — aliased from colors.lua for local-variable call performance.
|
||||
-- ---------------------------------------------------------------------------
|
||||
local clamp = eye_spy.colors.clamp
|
||||
local split_rgb = eye_spy.colors.split_rgb
|
||||
local join_rgb = eye_spy.colors.join_rgb
|
||||
local channel_luminance = eye_spy.colors.channel_luminance
|
||||
local color_luminance = eye_spy.colors.color_luminance
|
||||
local contrast_ratio = eye_spy.colors.contrast_ratio
|
||||
local scale_color = eye_spy.colors.scale_color
|
||||
local blend_color = eye_spy.colors.blend_color
|
||||
local rgb_to_hsl = eye_spy.colors.rgb_to_hsl
|
||||
local hsl_to_rgb = eye_spy.colors.hsl_to_rgb
|
||||
local get_bg_luminance_rgb = eye_spy.colors.get_bg_luminance_rgb
|
||||
local get_bg_luminance = eye_spy.colors.get_bg_luminance
|
||||
local best_monochrome_for_bg = eye_spy.colors.best_monochrome_for_bg
|
||||
local spectrum_color = eye_spy.colors.spectrum_color
|
||||
local adapt_for_bg = eye_spy.colors.adapt_for_bg
|
||||
local nudge_contrast = eye_spy.colors.nudge_contrast
|
||||
local tune_contrast_preserve_hue = eye_spy.colors.tune_contrast_preserve_hue
|
||||
|
||||
local function build_layout_lines(view_model)
|
||||
local fixed_slots = {}
|
||||
local append_lines = {}
|
||||
|
||||
for _, line in ipairs(view_model.lines or {}) do
|
||||
local fixed_row = line.id and FIXED_LINE_ROW_BY_ID[line.id] or nil
|
||||
|
||||
if fixed_row and fixed_slots[line.id] == nil then
|
||||
fixed_slots[line.id] = line
|
||||
else
|
||||
append_lines[#append_lines + 1] = line
|
||||
end
|
||||
end
|
||||
|
||||
local ordered_lines = {}
|
||||
|
||||
for _, line_id in ipairs(FIXED_LINE_ORDER) do
|
||||
if fixed_slots[line_id] then
|
||||
ordered_lines[#ordered_lines + 1] = fixed_slots[line_id]
|
||||
end
|
||||
end
|
||||
|
||||
for _, line in ipairs(append_lines) do
|
||||
ordered_lines[#ordered_lines + 1] = line
|
||||
end
|
||||
|
||||
local total_rows = #ordered_lines
|
||||
local required_rows = math.max(1, total_rows)
|
||||
local max_rows = BASE_LAYOUT_LINE_ROWS * MAX_LAYOUT_SCALE_FACTOR
|
||||
local visible_rows = math.min(required_rows, max_rows)
|
||||
local layout_lines = {}
|
||||
|
||||
for row = 1, visible_rows do
|
||||
layout_lines[row] = ordered_lines[row]
|
||||
end
|
||||
|
||||
local overflow_count = math.max(0, total_rows - visible_rows)
|
||||
|
||||
if overflow_count > 0 and visible_rows > 0 then
|
||||
layout_lines[visible_rows] = {
|
||||
id = "overflow",
|
||||
text = string.format(S("+%d more"), overflow_count),
|
||||
color = 0xAAAAAA,
|
||||
}
|
||||
end
|
||||
|
||||
return layout_lines, visible_rows
|
||||
end
|
||||
|
||||
local function estimate_panel_width(view_model, layout_lines)
|
||||
local max_len = math.max(#(view_model.title or ""), #(view_model.subtitle or ""), #(view_model.footer or ""))
|
||||
|
||||
for _, line in ipairs(layout_lines or view_model.lines or {}) do
|
||||
if line then
|
||||
max_len = math.max(max_len, #(line.text or ""))
|
||||
end
|
||||
end
|
||||
|
||||
local baseline_width = view_model.icon and 468 or 420
|
||||
local baseline_char_budget = view_model.icon and 176 or 208
|
||||
local grow_chars = math.max(0, max_len - baseline_char_budget)
|
||||
local required_width = baseline_width + math.floor(grow_chars * 6)
|
||||
|
||||
return clamp(required_width, baseline_width, baseline_width * MAX_LAYOUT_SCALE_FACTOR)
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function has_line(view_model, line_id)
|
||||
for _, line in ipairs(view_model.lines) do
|
||||
if line.id == line_id then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Hue offsets (degrees) spread each line type around the complementary of
|
||||
-- the background hue so they remain visually distinct from each other.
|
||||
local LINE_HUE_OFFSETS = {
|
||||
growth = 0,
|
||||
soil = 30,
|
||||
dig_time = 55,
|
||||
light = 85,
|
||||
spawn = 45,
|
||||
liquid = -50,
|
||||
}
|
||||
|
||||
local auto_line_color_cache = {}
|
||||
local auto_line_color_cache_size = 0
|
||||
|
||||
local function get_auto_line_accent(line_id, bg_hex, bg_color, base_text, bg_luminance)
|
||||
local cache_key = table.concat({ line_id or "", bg_hex or "", tostring(base_text) }, "|")
|
||||
local cached = auto_line_color_cache[cache_key]
|
||||
|
||||
if cached then
|
||||
return cached
|
||||
end
|
||||
|
||||
local bg_r, bg_g, bg_b = split_rgb(bg_color)
|
||||
local bg_h, bg_s, bg_l = rgb_to_hsl(bg_r, bg_g, bg_b)
|
||||
|
||||
-- Dynamic accent: complementary hue + per-line offset, lightness inversely
|
||||
-- proportional to bg lightness so it remains readable on any saturated bg
|
||||
-- without collapsing to a monochrome colour.
|
||||
local hue_off = LINE_HUE_OFFSETS[line_id] or 0
|
||||
local accent_h = (bg_h + 180 + hue_off) % 360
|
||||
local accent_s = clamp(0.60 + bg_s * 0.22, 0.48, 0.88)
|
||||
local accent_l = clamp(0.76 - bg_l * 0.52, 0.20, 0.82)
|
||||
local dr, dg, db = hsl_to_rgb(accent_h, accent_s, accent_l)
|
||||
local dynamic = join_rgb(dr, dg, db)
|
||||
|
||||
-- Fixed palettes are still used as the starting point for near-neutral
|
||||
-- backgrounds where the bg hue is undefined or very weak.
|
||||
local dark_palette = {
|
||||
growth = 0x8BE68B,
|
||||
soil = 0xE7C882,
|
||||
dig_time = 0xAFC9FF,
|
||||
light = 0xF2DE98,
|
||||
spawn = 0xF2B189,
|
||||
liquid = 0x8CC8FF,
|
||||
}
|
||||
local light_palette = {
|
||||
growth = 0x2D7A35,
|
||||
soil = 0x7E5A1F,
|
||||
dig_time = 0x345C98,
|
||||
light = 0x725C1D,
|
||||
spawn = 0x94442A,
|
||||
liquid = 0x1E5E9A,
|
||||
}
|
||||
local lightness_t = clamp((bg_l - 0.35) / 0.30, 0, 1)
|
||||
local dark_fixed = dark_palette[line_id] or dynamic
|
||||
local light_fixed = light_palette[line_id] or dynamic
|
||||
local fixed = blend_color(dark_fixed, light_fixed, lightness_t)
|
||||
|
||||
-- Blend: saturated bg → trust dynamic complementary; neutral bg → fixed.
|
||||
local accent = blend_color(fixed, dynamic, clamp(bg_s * 1.3, 0, 1))
|
||||
|
||||
-- Contrast pass without hard-snapping to monochrome fallback.
|
||||
local result = tune_contrast_preserve_hue(accent, bg_color, 3.6)
|
||||
result = nudge_contrast(result, bg_color, 3.8)
|
||||
|
||||
if auto_line_color_cache_size > 512 then
|
||||
auto_line_color_cache = {}
|
||||
auto_line_color_cache_size = 0
|
||||
end
|
||||
|
||||
if not auto_line_color_cache[cache_key] then
|
||||
auto_line_color_cache_size = auto_line_color_cache_size + 1
|
||||
end
|
||||
|
||||
auto_line_color_cache[cache_key] = result
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function adjust_semantic_line_color(color, bg_color)
|
||||
return nudge_contrast(
|
||||
tune_contrast_preserve_hue(color, bg_color, 3.6),
|
||||
bg_color,
|
||||
3.8
|
||||
)
|
||||
end
|
||||
|
||||
local function get_line_color_for_manual(meta, line_id)
|
||||
if line_id == "growth" then
|
||||
return spectrum_color(meta:get_int("es_growth_color_val"))
|
||||
end
|
||||
|
||||
if line_id == "soil" then
|
||||
return spectrum_color(meta:get_int("es_soil_color_val"))
|
||||
end
|
||||
|
||||
return spectrum_color(meta:get_int("es_line_color_val"))
|
||||
end
|
||||
|
||||
local function position_text(pos)
|
||||
if not pos then
|
||||
return "0, 0, 0"
|
||||
end
|
||||
|
||||
return string.format("%s, %s, %s", eye_spy.round1(pos.x) or 0, eye_spy.round1(pos.y) or 0, eye_spy.round1(pos.z) or 0)
|
||||
end
|
||||
|
||||
local function texture_text(target)
|
||||
if target and target.kind == "entity" then
|
||||
return ENTITY_ICON_TEXTURE .. "^[resize:" .. ICON_TEXTURE_SIZE .. "x" .. ICON_TEXTURE_SIZE
|
||||
end
|
||||
|
||||
local texture = target.texture or "eye_spy_default_texture.png"
|
||||
|
||||
if type(texture) == "table" then
|
||||
texture = texture.name or texture.image or "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
if type(texture) ~= "string" or texture == "" then
|
||||
texture = "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
texture = eye_spy.strip_texture_modifiers(texture)
|
||||
|
||||
if target.color then
|
||||
texture = texture .. "^[multiply:" .. target.color
|
||||
elseif target.palette then
|
||||
texture = texture .. "^[multiply:#7CBD6B"
|
||||
end
|
||||
|
||||
-- Normalize every icon texture to the same source size so HUD icon size is consistent.
|
||||
texture = texture .. "^[resize:" .. ICON_TEXTURE_SIZE .. "x" .. ICON_TEXTURE_SIZE
|
||||
|
||||
return texture
|
||||
end
|
||||
|
||||
-- Per-player layout signature cache. The 35-key meta scan only needs to run
|
||||
-- when the player is actively changing layout values in the editor. All other
|
||||
-- updates (normal play, colour slider dragging) reuse the cached string.
|
||||
local player_layout_sigs = {}
|
||||
|
||||
local function get_player_draft(player)
|
||||
if not player or not eye_spy.ui_drafts then
|
||||
return nil
|
||||
end
|
||||
|
||||
return eye_spy.ui_drafts[player:get_player_name()]
|
||||
end
|
||||
|
||||
local function get_effective_string(meta, draft, key)
|
||||
if draft and draft[key] ~= nil then
|
||||
return tostring(draft[key])
|
||||
end
|
||||
|
||||
return meta:get_string(key)
|
||||
end
|
||||
|
||||
local function get_effective_int(meta, draft, key)
|
||||
if draft and draft[key] ~= nil then
|
||||
return tonumber(draft[key]) or 0
|
||||
end
|
||||
|
||||
return meta:get_int(key)
|
||||
end
|
||||
|
||||
function eye_spy.render.invalidate_layout_sig(player_name)
|
||||
player_layout_sigs[player_name] = nil
|
||||
end
|
||||
|
||||
local function get_layout_signature(meta, player_name)
|
||||
if player_name and player_layout_sigs[player_name] then
|
||||
return player_layout_sigs[player_name]
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
|
||||
for _, key in ipairs(layout_key_names) do
|
||||
parts[#parts + 1] = tostring(meta:get_int(key))
|
||||
end
|
||||
|
||||
local sig = table.concat(parts, ",")
|
||||
|
||||
if player_name then
|
||||
player_layout_sigs[player_name] = sig
|
||||
end
|
||||
|
||||
return sig
|
||||
end
|
||||
|
||||
local function get_layout_signature_from_values(meta, draft)
|
||||
local parts = {}
|
||||
|
||||
for _, key in ipairs(layout_key_names) do
|
||||
parts[#parts + 1] = tostring(get_effective_int(meta, draft, key))
|
||||
end
|
||||
|
||||
return table.concat(parts, ",")
|
||||
end
|
||||
|
||||
local function build_key(view_model)
|
||||
local parts = {
|
||||
RENDER_LAYOUT_REV,
|
||||
view_model.layout_signature or "",
|
||||
view_model.layout,
|
||||
view_model.bg_color,
|
||||
view_model.title,
|
||||
tostring(view_model.title_color),
|
||||
view_model.subtitle,
|
||||
tostring(view_model.subtitle_color),
|
||||
view_model.footer,
|
||||
tostring(view_model.footer_color),
|
||||
view_model.icon and view_model.icon.text or "",
|
||||
tostring(#view_model.lines),
|
||||
}
|
||||
|
||||
for _, line in ipairs(view_model.lines) do
|
||||
parts[#parts + 1] = line.id
|
||||
parts[#parts + 1] = line.text
|
||||
parts[#parts + 1] = tostring(line.color)
|
||||
end
|
||||
|
||||
return table.concat(parts, "|")
|
||||
end
|
||||
|
||||
local function hud_elem_type(def)
|
||||
return def and (def.type or def.hud_elem_type)
|
||||
end
|
||||
|
||||
local function ensure_hud_slot(player, hud_ids, key, expected_type, spec)
|
||||
local id = hud_ids[key]
|
||||
|
||||
if id then
|
||||
local def = player:hud_get(id)
|
||||
|
||||
if not def or hud_elem_type(def) ~= expected_type then
|
||||
if def then
|
||||
player:hud_remove(id)
|
||||
end
|
||||
|
||||
hud_ids[key] = nil
|
||||
id = nil
|
||||
end
|
||||
end
|
||||
|
||||
if not id then
|
||||
hud_ids[key] = player:hud_add(spec)
|
||||
end
|
||||
end
|
||||
|
||||
function eye_spy.render.ensure_hud(state, player)
|
||||
local hud_ids = state.hud_ids
|
||||
|
||||
ensure_hud_slot(player, hud_ids, "bg", "image", {
|
||||
type = "image",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
scale = { x = 400, y = 82 },
|
||||
alignment = { x = 0, y = 0 },
|
||||
z_index = 1,
|
||||
})
|
||||
|
||||
ensure_hud_slot(player, hud_ids, "icon", "image", {
|
||||
type = "image",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
scale = { x = 3.5, y = 3.5 },
|
||||
alignment = { x = 0, y = 0 },
|
||||
z_index = 2,
|
||||
})
|
||||
|
||||
ensure_hud_slot(player, hud_ids, "title", "text", {
|
||||
type = "text",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
number = 0xFFFFFF,
|
||||
alignment = { x = 1, y = 0 },
|
||||
z_index = 2,
|
||||
})
|
||||
|
||||
ensure_hud_slot(player, hud_ids, "subtitle", "text", {
|
||||
type = "text",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
number = 0xEBB344,
|
||||
alignment = { x = 1, y = 0 },
|
||||
z_index = 2,
|
||||
})
|
||||
|
||||
ensure_hud_slot(player, hud_ids, "footer", "text", {
|
||||
type = "text",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
number = 0x4343F0,
|
||||
alignment = { x = 1, y = 0 },
|
||||
z_index = 2,
|
||||
})
|
||||
end
|
||||
|
||||
function eye_spy.render.ensure_line_slots(state, player, count)
|
||||
local line_ids = state.hud_ids.lines
|
||||
|
||||
for i = 1, #line_ids do
|
||||
local id = line_ids[i]
|
||||
local def = id and player:hud_get(id)
|
||||
|
||||
if not def or hud_elem_type(def) ~= "text" then
|
||||
if def then
|
||||
player:hud_remove(id)
|
||||
end
|
||||
|
||||
line_ids[i] = player:hud_add({
|
||||
type = "text",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
number = 0xFFFFFF,
|
||||
alignment = { x = 1, y = 0 },
|
||||
z_index = 2,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
while #line_ids < count do
|
||||
line_ids[#line_ids + 1] = player:hud_add({
|
||||
type = "text",
|
||||
position = { x = 0.5, y = 0 },
|
||||
offset = { x = 0, y = 0 },
|
||||
text = "",
|
||||
number = 0xFFFFFF,
|
||||
alignment = { x = 1, y = 0 },
|
||||
z_index = 2,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function eye_spy.render.hide(state, player)
|
||||
if not state or not state.hud_ids.bg then
|
||||
return
|
||||
end
|
||||
|
||||
local hud_ids = state.hud_ids
|
||||
|
||||
player:hud_change(hud_ids.bg, "text", "")
|
||||
player:hud_change(hud_ids.icon, "text", "")
|
||||
player:hud_change(hud_ids.title, "text", "")
|
||||
player:hud_change(hud_ids.subtitle, "text", "")
|
||||
player:hud_change(hud_ids.footer, "text", "")
|
||||
|
||||
for _, line_id in ipairs(hud_ids.lines) do
|
||||
player:hud_change(line_id, "text", "")
|
||||
end
|
||||
end
|
||||
|
||||
function eye_spy.render.remove(player)
|
||||
local state = eye_spy.get_player_state(player)
|
||||
local hud_ids = state.hud_ids
|
||||
|
||||
for key, value in pairs(hud_ids) do
|
||||
if key == "lines" then
|
||||
for _, line_id in ipairs(value) do
|
||||
if line_id and player:hud_get(line_id) then
|
||||
player:hud_remove(line_id)
|
||||
end
|
||||
end
|
||||
elseif value and player:hud_get(value) then
|
||||
player:hud_remove(value)
|
||||
end
|
||||
end
|
||||
|
||||
state.hud_ids = { lines = {} }
|
||||
state.last_render_key = nil
|
||||
state.last_geom_key = nil
|
||||
end
|
||||
|
||||
function eye_spy.render.build_view_model(player, target, opts)
|
||||
local perf_on = eye_spy.perf and eye_spy.perf.enabled and minetest.get_us_time
|
||||
local phase_start_us = perf_on and minetest.get_us_time() or 0
|
||||
local meta = player:get_meta()
|
||||
local options = opts or {}
|
||||
local draft = get_player_draft(player)
|
||||
local effective_meta = meta
|
||||
|
||||
if draft then
|
||||
effective_meta = {
|
||||
get_string = function(_, key)
|
||||
return get_effective_string(meta, draft, key)
|
||||
end,
|
||||
get_int = function(_, key)
|
||||
return get_effective_int(meta, draft, key)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local r, g, b = eye_spy.get_rgb(get_effective_string(meta, draft, "es_hud_color"))
|
||||
local bg_luminance = get_bg_luminance_rgb(r or 26, g or 26, b or 27)
|
||||
local bg_color_int = join_rgb(r or 26, g or 26, b or 27)
|
||||
local auto_text = get_effective_string(meta, draft, "es_auto_text_color") ~= "false"
|
||||
local footer_mode = get_effective_string(meta, draft, "es_footer_mode")
|
||||
local show_coords = get_effective_string(meta, draft, "es_show_coords") ~= "false"
|
||||
local footer_text
|
||||
local coords_text = show_coords and (" " .. position_text(target.pos)) or ""
|
||||
|
||||
if footer_mode == "advanced" then
|
||||
local full_name = target.full_name or target.name or S("Unknown")
|
||||
footer_text = "[" .. full_name .. "] [" .. (target.modname or S("Unknown")) .. "]" .. coords_text
|
||||
else
|
||||
footer_text = "[" .. (target.modname or S("Unknown")) .. "]" .. coords_text
|
||||
end
|
||||
|
||||
local view_model = {
|
||||
visible = target.kind ~= "air",
|
||||
layout = get_effective_string(meta, draft, "es_hud_alignment") ~= "" and
|
||||
get_effective_string(meta, draft, "es_hud_alignment") or "Top-Middle",
|
||||
bg_color = eye_spy.rgb_to_hex(r or 26, g or 26, b or 27),
|
||||
title = target.description or target.name or S("Unknown"),
|
||||
title_color = 0xFFFFFF,
|
||||
subtitle = target.kind == "entity" and S("Entity") or (target.kind == "item" and S("Item") or S("Node")),
|
||||
subtitle_color = 0xF2D23C,
|
||||
footer = footer_text,
|
||||
footer_color = 0x4C7EE8,
|
||||
lines = {},
|
||||
layout_signature = draft and get_layout_signature_from_values(meta, draft) or
|
||||
get_layout_signature(meta, player:get_player_name()),
|
||||
}
|
||||
|
||||
if get_effective_string(meta, draft, "es_show_icons") ~= "false" then
|
||||
local icon_size = clamp(get_effective_int(meta, draft, "es_icon_size"), 16, 128)
|
||||
|
||||
if icon_size <= 0 then
|
||||
icon_size = ICON_DISPLAY_SIZE
|
||||
end
|
||||
|
||||
view_model.icon = {
|
||||
text = texture_text(target),
|
||||
scale = { x = icon_size / ICON_TEXTURE_SIZE, y = icon_size / ICON_TEXTURE_SIZE },
|
||||
}
|
||||
end
|
||||
|
||||
eye_spy.enrichers.run({ player = player, meta = effective_meta, preview = options.preview }, target, view_model)
|
||||
if perf_on then
|
||||
eye_spy.perf_record("build_vm_enrichers", minetest.get_us_time() - phase_start_us)
|
||||
phase_start_us = minetest.get_us_time()
|
||||
end
|
||||
|
||||
local show_growth = get_effective_string(meta, draft, "es_show_growth") ~= "false"
|
||||
|
||||
if options.preview and target.kind == "node" then
|
||||
if show_growth and not has_line(view_model, "growth") then
|
||||
view_model.lines[#view_model.lines + 1] = {
|
||||
id = "growth",
|
||||
text = S("Growth") .. ": " .. S("Preview Sample"),
|
||||
color = 0x55FF55,
|
||||
}
|
||||
end
|
||||
|
||||
if show_growth and not has_line(view_model, "soil") then
|
||||
view_model.lines[#view_model.lines + 1] = {
|
||||
id = "soil",
|
||||
text = S("Soil") .. ": " .. S("Preview Sample"),
|
||||
color = 0xD8B46A,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if auto_text then
|
||||
local base_text = best_monochrome_for_bg(bg_color_int)
|
||||
|
||||
view_model.title_color = base_text
|
||||
-- Subtitle/footer: preserve semantic colour (amber for tool, red for health,
|
||||
-- blue for footer) and nudge toward readable contrast without snapping to
|
||||
-- a monochrome fallback.
|
||||
view_model.subtitle_color = nudge_contrast(
|
||||
tune_contrast_preserve_hue(view_model.subtitle_color, bg_color_int, 3.6),
|
||||
bg_color_int, 3.8
|
||||
)
|
||||
view_model.footer_color = nudge_contrast(
|
||||
tune_contrast_preserve_hue(view_model.footer_color, bg_color_int, 3.6),
|
||||
bg_color_int, 3.8
|
||||
)
|
||||
|
||||
for _, line in ipairs(view_model.lines) do
|
||||
if line.id == "growth" or line.id == "soil" then
|
||||
line.color = adjust_semantic_line_color(line.color, bg_color_int)
|
||||
else
|
||||
line.color = get_auto_line_accent(line.id, view_model.bg_color, bg_color_int, base_text, bg_luminance)
|
||||
end
|
||||
end
|
||||
else
|
||||
view_model.title_color = spectrum_color(get_effective_int(meta, draft, "es_title_color_val"))
|
||||
view_model.subtitle_color = spectrum_color(get_effective_int(meta, draft, "es_subtitle_color_val"))
|
||||
view_model.footer_color = spectrum_color(get_effective_int(meta, draft, "es_footer_color_val"))
|
||||
|
||||
for _, line in ipairs(view_model.lines) do
|
||||
if line.id == "growth" or line.id == "soil" then
|
||||
line.color = adjust_semantic_line_color(line.color, bg_color_int)
|
||||
else
|
||||
line.color = spectrum_color(get_effective_int(meta, draft, "es_line_color_val"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if perf_on then
|
||||
eye_spy.perf_record("build_vm_color_pass", minetest.get_us_time() - phase_start_us)
|
||||
phase_start_us = minetest.get_us_time()
|
||||
end
|
||||
|
||||
view_model.key = build_key(view_model)
|
||||
|
||||
if perf_on then
|
||||
eye_spy.perf_record("build_vm_key_build", minetest.get_us_time() - phase_start_us)
|
||||
end
|
||||
|
||||
return view_model
|
||||
end
|
||||
|
||||
function eye_spy.render.apply(state, player, view_model)
|
||||
local layout_lines, visible_line_rows = build_layout_lines(view_model)
|
||||
|
||||
eye_spy.render.ensure_hud(state, player)
|
||||
eye_spy.render.ensure_line_slots(state, player, visible_line_rows)
|
||||
|
||||
local meta = player:get_meta()
|
||||
local draft = get_player_draft(player)
|
||||
local layout = layouts[view_model.layout] or layouts["Top-Middle"]
|
||||
local line_count = visible_line_rows
|
||||
local global_offset_x = get_effective_int(meta, draft, "es_global_offset_x")
|
||||
local global_offset_y = get_effective_int(meta, draft, "es_global_offset_y")
|
||||
local bg_offset_x = get_effective_int(meta, draft, "es_bg_offset_x")
|
||||
local bg_offset_y = get_effective_int(meta, draft, "es_bg_offset_y")
|
||||
local icon_offset_x = get_effective_int(meta, draft, "es_icon_offset_x")
|
||||
local icon_offset_y = get_effective_int(meta, draft, "es_icon_offset_y")
|
||||
local title_offset_x = get_effective_int(meta, draft, "es_title_offset_x")
|
||||
local title_offset_y = get_effective_int(meta, draft, "es_title_offset_y")
|
||||
local subtitle_offset_x = get_effective_int(meta, draft, "es_subtitle_offset_x")
|
||||
local subtitle_offset_y = get_effective_int(meta, draft, "es_subtitle_offset_y")
|
||||
local lines_offset_x = get_effective_int(meta, draft, "es_lines_offset_x")
|
||||
local lines_offset_y = get_effective_int(meta, draft, "es_lines_offset_y")
|
||||
local footer_offset_x = get_effective_int(meta, draft, "es_footer_offset_x")
|
||||
local footer_offset_y = get_effective_int(meta, draft, "es_footer_offset_y")
|
||||
local bg_pad_left = get_effective_int(meta, draft, "es_bg_pad_left")
|
||||
local bg_pad_right = get_effective_int(meta, draft, "es_bg_pad_right")
|
||||
local bg_pad_top = get_effective_int(meta, draft, "es_bg_pad_top")
|
||||
local bg_pad_bottom = get_effective_int(meta, draft, "es_bg_pad_bottom")
|
||||
local bg_extra_w = get_effective_int(meta, draft, "es_bg_extra_w")
|
||||
local bg_extra_h = get_effective_int(meta, draft, "es_bg_extra_h")
|
||||
local text_base_x_adj = get_effective_int(meta, draft, "es_text_base_x_adj")
|
||||
local icon_base_x_adj = get_effective_int(meta, draft, "es_icon_base_x_adj")
|
||||
local top_margin_adj = get_effective_int(meta, draft, "es_top_margin_adj")
|
||||
local bottom_margin_adj = get_effective_int(meta, draft, "es_bottom_margin_adj")
|
||||
|
||||
local title_y = 16 + title_offset_y
|
||||
local subtitle_y = 34 + subtitle_offset_y
|
||||
local first_line_y = 52 + get_effective_int(meta, draft, "es_first_line_y_adj")
|
||||
local line_step = math.max(10, 18 + get_effective_int(meta, draft, "es_line_step_adj"))
|
||||
local footer_nudge = -10 + get_effective_int(meta, draft, "es_footer_nudge_adj")
|
||||
local footer_y = first_line_y + (line_count * line_step) + 10 + footer_nudge
|
||||
|
||||
local content_top = title_y - 8 - bg_pad_top
|
||||
local content_bottom = footer_y + 22 + bg_pad_bottom
|
||||
local base_footer_y = first_line_y + (BASE_LAYOUT_LINE_ROWS * line_step) + 10 + footer_nudge
|
||||
local base_content_bottom = base_footer_y + 22 + bg_pad_bottom
|
||||
local base_bg_height = (base_content_bottom - content_top) + bg_extra_h
|
||||
local max_bg_height = math.max(52, math.floor(base_bg_height * MAX_LAYOUT_SCALE_FACTOR))
|
||||
local bg_height = (content_bottom - content_top) + bg_extra_h
|
||||
local bg_width = estimate_panel_width(view_model, layout_lines) + bg_pad_left + bg_pad_right + bg_extra_w
|
||||
local top_screen_margin = 10 + top_margin_adj
|
||||
local bottom_screen_margin = 22 + bottom_margin_adj
|
||||
|
||||
bg_height = clamp(bg_height, 52, max_bg_height)
|
||||
bg_width = clamp(bg_width, 240, 1260)
|
||||
|
||||
local origin_y
|
||||
|
||||
if layout.y_mode == "top" then
|
||||
origin_y = top_screen_margin
|
||||
elseif layout.y_mode == "middle" then
|
||||
origin_y = -math.floor(bg_height / 2)
|
||||
else
|
||||
origin_y = -(bg_height + bottom_screen_margin)
|
||||
end
|
||||
|
||||
origin_y = origin_y + global_offset_y
|
||||
|
||||
local center_x = global_offset_x
|
||||
|
||||
if layout.x_mode == "right" then
|
||||
center_x = -math.floor(bg_width / 2) - 16
|
||||
elseif layout.x_mode == "left" then
|
||||
center_x = math.floor(bg_width / 2) + 16
|
||||
end
|
||||
|
||||
local left_edge_x = center_x - math.floor(bg_width / 2)
|
||||
local icon_x = left_edge_x + 28 + bg_pad_left + icon_base_x_adj + icon_offset_x
|
||||
local text_x = left_edge_x + (view_model.icon and 68 or 16) + bg_pad_left + text_base_x_adj
|
||||
|
||||
local bg_scale_x_pct = clamp(get_effective_int(meta, draft, "es_bg_scale_x_pct"), 25, 300)
|
||||
local bg_scale_y_pct = clamp(get_effective_int(meta, draft, "es_bg_scale_y_pct"), 25, 300)
|
||||
local bg_draw_w = math.max(1, math.floor(bg_width * (bg_scale_x_pct / 100)))
|
||||
local bg_draw_h = math.max(1, math.floor(bg_height * (bg_scale_y_pct / 100)))
|
||||
|
||||
local title_scale = clamp(get_effective_int(meta, draft, "es_title_scale_pct"), 40, 300) / 100
|
||||
local subtitle_scale = clamp(get_effective_int(meta, draft, "es_subtitle_scale_pct"), 40, 300) / 100
|
||||
local line_scale = clamp(get_effective_int(meta, draft, "es_line_scale_pct"), 40, 300) / 100
|
||||
local footer_scale = clamp(get_effective_int(meta, draft, "es_footer_scale_pct"), 40, 300) / 100
|
||||
|
||||
local bg_center_y = origin_y + math.floor(bg_height / 2) - bg_pad_top
|
||||
|
||||
-- Geometry channel: position, offset, scale.
|
||||
-- Only sent when layout or panel dimensions actually changed.
|
||||
local geom_key = (view_model.layout or "")
|
||||
.. "|" .. (view_model.layout_signature or "")
|
||||
.. "|" .. tostring(bg_draw_w)
|
||||
.. "|" .. tostring(bg_draw_h)
|
||||
.. "|" .. tostring(line_count)
|
||||
.. "|" .. (view_model.icon and tostring(math.floor(view_model.icon.scale.x * 64)) or "0")
|
||||
|
||||
if geom_key ~= (state.last_geom_key or "") then
|
||||
state.last_geom_key = geom_key
|
||||
|
||||
player:hud_change(state.hud_ids.bg, "position", layout.pos)
|
||||
player:hud_change(state.hud_ids.bg, "offset", { x = center_x + bg_offset_x, y = bg_center_y + bg_offset_y })
|
||||
player:hud_change(state.hud_ids.bg, "scale", { x = bg_draw_w, y = bg_draw_h })
|
||||
|
||||
player:hud_change(state.hud_ids.title, "position", layout.pos)
|
||||
player:hud_change(state.hud_ids.title, "offset", { x = text_x + title_offset_x, y = origin_y + title_y })
|
||||
player:hud_change(state.hud_ids.title, "scale", { x = title_scale, y = title_scale })
|
||||
|
||||
player:hud_change(state.hud_ids.subtitle, "position", layout.pos)
|
||||
player:hud_change(state.hud_ids.subtitle, "offset", { x = text_x + subtitle_offset_x, y = origin_y + subtitle_y })
|
||||
player:hud_change(state.hud_ids.subtitle, "scale", { x = subtitle_scale, y = subtitle_scale })
|
||||
|
||||
player:hud_change(state.hud_ids.footer, "position", layout.pos)
|
||||
player:hud_change(state.hud_ids.footer, "offset",
|
||||
{ x = text_x + footer_offset_x, y = origin_y + footer_y + footer_offset_y })
|
||||
player:hud_change(state.hud_ids.footer, "scale", { x = footer_scale, y = footer_scale })
|
||||
|
||||
if view_model.icon then
|
||||
player:hud_change(state.hud_ids.icon, "position", layout.pos)
|
||||
player:hud_change(state.hud_ids.icon, "offset", { x = icon_x, y = origin_y + 34 + icon_offset_y })
|
||||
player:hud_change(state.hud_ids.icon, "scale", view_model.icon.scale)
|
||||
end
|
||||
|
||||
-- Geometry for all allocated line slots so new/reactivated slots are positioned correctly.
|
||||
for index, line_id in ipairs(state.hud_ids.lines) do
|
||||
player:hud_change(line_id, "position", layout.pos)
|
||||
player:hud_change(line_id, "offset", {
|
||||
x = text_x + lines_offset_x,
|
||||
y = origin_y + first_line_y + ((index - 1) * line_step) + lines_offset_y,
|
||||
})
|
||||
player:hud_change(line_id, "scale", { x = line_scale, y = line_scale })
|
||||
end
|
||||
end
|
||||
|
||||
-- Content channel: text, number/color -- always applied when view_model changed.
|
||||
player:hud_change(state.hud_ids.bg, "text", "eye_spy_hud_bg.png^[multiply:#" .. view_model.bg_color)
|
||||
|
||||
player:hud_change(state.hud_ids.title, "text", view_model.title)
|
||||
player:hud_change(state.hud_ids.title, "number", view_model.title_color)
|
||||
|
||||
player:hud_change(state.hud_ids.subtitle, "text", view_model.subtitle)
|
||||
player:hud_change(state.hud_ids.subtitle, "number", view_model.subtitle_color)
|
||||
|
||||
player:hud_change(state.hud_ids.footer, "text", view_model.footer)
|
||||
player:hud_change(state.hud_ids.footer, "number", view_model.footer_color)
|
||||
|
||||
if view_model.icon then
|
||||
player:hud_change(state.hud_ids.icon, "text", view_model.icon.text)
|
||||
else
|
||||
player:hud_change(state.hud_ids.icon, "text", "")
|
||||
end
|
||||
|
||||
for index, line_id in ipairs(state.hud_ids.lines) do
|
||||
local line = layout_lines[index]
|
||||
|
||||
if line then
|
||||
player:hud_change(line_id, "text", line.text)
|
||||
player:hud_change(line_id, "number", line.color)
|
||||
else
|
||||
player:hud_change(line_id, "text", "")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 428 KiB |
@@ -0,0 +1,60 @@
|
||||
[eye_spy]
|
||||
|
||||
[Entities]
|
||||
|
||||
# Shows entities you are looking at, displaying their name, health, and type.
|
||||
eye_spy.show_entities (Show Entities) bool true
|
||||
|
||||
[Liquids]
|
||||
|
||||
# Enables liquid-aware targeting. When enabled, Eye Spy can scan through liquid
|
||||
# surfaces, show the liquid line, and still show the target block behind it.
|
||||
eye_spy.show_liquids (Show Liquids) bool false
|
||||
|
||||
[Display]
|
||||
|
||||
# Always shows "Hand" as the required tool in creative mode, regardless of node type.
|
||||
eye_spy.always_show_hand_in_creative (Always Show "Hand" in Creative Mode) bool true
|
||||
|
||||
# Forces all item/node icons in the HUD to render at this pixel size.
|
||||
# When set to None, each texture is displayed at its native size.
|
||||
# Note: when mod security is disabled, sizes are detected automatically.
|
||||
eye_spy.texture_pack_size (Texture Pack Size) enum None None,1x1px,2x2px,4x4px,8x8px,16x16px,32x32px,64x64px,128x128px,256x256px,512x512px,1024x1024px
|
||||
|
||||
# Shows growth progress and soil condition lines for supported plants and saplings.
|
||||
eye_spy.show_growth (Show Growth Details) bool true
|
||||
|
||||
# Shows a small icon representing the current target in the HUD panel.
|
||||
eye_spy.show_icons (Show Target Icon) bool true
|
||||
|
||||
[Performance]
|
||||
|
||||
# Sets the initial server-wide HUD update interval in seconds.
|
||||
# This value is only applied on the first server start if no override has been
|
||||
# saved via /eye_spy_rate. After that, use /eye_spy_rate to change the interval
|
||||
# at runtime without restarting the server.
|
||||
eye_spy.update_interval (Update Interval) float 0.1 0.05 5.0
|
||||
|
||||
# Enable lightweight Eye Spy performance metrics collection.
|
||||
# Can also be toggled at runtime with /eye_spy_perf on|off.
|
||||
eye_spy.enable_perf_metrics (Enable Performance Metrics) bool false
|
||||
|
||||
[Defaults]
|
||||
|
||||
# Default HUD visibility settings applied to new players on first join.
|
||||
# Players can override all of these individually in /eye_spy_settings.
|
||||
|
||||
# Show coordinates of the targeted block/entity in the HUD footer.
|
||||
eye_spy.default_show_coords (Default: Show Coords in Footer) bool false
|
||||
|
||||
# Show the ambient light level at the targeted block.
|
||||
eye_spy.default_show_light_level (Default: Show Light Level) bool true
|
||||
|
||||
# Show a mob-spawn risk estimate based on light level at the targeted block.
|
||||
eye_spy.default_show_spawn_hint (Default: Show Spawn Hint) bool false
|
||||
|
||||
# Show liquid name and level when looking at or through liquid.
|
||||
eye_spy.default_show_liquid_info (Default: Show Liquid Info) bool true
|
||||
|
||||
# Show the estimated time to dig the targeted block with the current tool.
|
||||
eye_spy.default_show_dig_time (Default: Show Dig Time) bool true
|
||||
@@ -0,0 +1,532 @@
|
||||
local S = eye_spy.S
|
||||
|
||||
eye_spy.target = {}
|
||||
|
||||
-- Returns the first line of a string, or nil if empty.
|
||||
local function first_line(text)
|
||||
if not text or text == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
return text:match("^[^\n]*")
|
||||
end
|
||||
|
||||
-- Returns a compact cache key string for a position.
|
||||
local function pos_key(pos)
|
||||
return string.format("%d,%d,%d", pos.x, pos.y, pos.z)
|
||||
end
|
||||
|
||||
-- Extracts the best available texture from a node definition.
|
||||
local function extract_node_texture(def)
|
||||
if not def or not def.tiles then
|
||||
return "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
return def.tiles[6] or def.tiles[1] or "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
-- Normalises a single tile entry (string or table) to a texture string.
|
||||
local function extract_tile_texture(tile)
|
||||
if type(tile) == "table" then
|
||||
return tile.name or tile.image or ""
|
||||
end
|
||||
|
||||
return type(tile) == "string" and tile or ""
|
||||
end
|
||||
|
||||
-- Extracts the best available texture from an item/node definition.
|
||||
local function extract_item_texture(item_name, def)
|
||||
local item_def = def or (item_name and minetest.registered_items[item_name]) or nil
|
||||
|
||||
if not item_def then
|
||||
return "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
if item_def.inventory_image and item_def.inventory_image ~= "" then
|
||||
return item_def.inventory_image
|
||||
end
|
||||
|
||||
if item_def.wield_image and item_def.wield_image ~= "" then
|
||||
return item_def.wield_image
|
||||
end
|
||||
|
||||
if item_def.tiles then
|
||||
local tile = extract_tile_texture(item_def.tiles[1])
|
||||
|
||||
if tile ~= "" then
|
||||
return tile
|
||||
end
|
||||
end
|
||||
|
||||
return "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
-- Extracts the first texture from a luaentity definition.
|
||||
local function extract_entity_texture(entity)
|
||||
if not entity then
|
||||
return "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
if entity.initial_properties and entity.initial_properties.textures then
|
||||
return entity.initial_properties.textures[1] or "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
if entity.textures then
|
||||
return entity.textures[1] or "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
return "eye_spy_default_texture.png"
|
||||
end
|
||||
|
||||
-- Attempts to retrieve an ItemStack carried by a dropped-item entity.
|
||||
local function get_entity_itemstack(entity)
|
||||
if not entity then
|
||||
return nil
|
||||
end
|
||||
|
||||
local raw = entity.itemstring or entity._itemstring or entity.item or entity.stackstring
|
||||
local stack
|
||||
|
||||
if type(raw) == "string" and raw ~= "" then
|
||||
stack = ItemStack(raw)
|
||||
elseif raw and type(raw) == "userdata" and raw.get_name then
|
||||
stack = raw
|
||||
end
|
||||
|
||||
if stack and not stack:is_empty() then
|
||||
return stack
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Returns a brief summary of item-stack metadata for display purposes.
|
||||
-- Returns: summary_string, has_meta (bool), has_custom_description (bool)
|
||||
local function summarize_item_meta(stack)
|
||||
if not stack or stack:is_empty() then
|
||||
return nil, false, false
|
||||
end
|
||||
|
||||
local meta = stack:get_meta()
|
||||
|
||||
if not meta then
|
||||
return nil, false, false
|
||||
end
|
||||
|
||||
local custom_description = first_line(meta:get_string("description"))
|
||||
local table_data = meta:to_table() or {}
|
||||
local fields = table_data.fields or {}
|
||||
local field_count = 0
|
||||
|
||||
for key, value in pairs(fields) do
|
||||
if key ~= "description" and value ~= nil and tostring(value) ~= "" then
|
||||
field_count = field_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
local inv_count = 0
|
||||
local inventory = table_data.inventory or {}
|
||||
|
||||
for _, list in pairs(inventory) do
|
||||
if type(list) == "table" and #list > 0 then
|
||||
inv_count = inv_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
|
||||
if field_count > 0 then
|
||||
parts[#parts + 1] = tostring(field_count) .. "f"
|
||||
end
|
||||
|
||||
if inv_count > 0 then
|
||||
parts[#parts + 1] = tostring(inv_count) .. "inv"
|
||||
end
|
||||
|
||||
local has_meta = #parts > 0
|
||||
|
||||
return table.concat(parts, ", "), has_meta, custom_description ~= nil and custom_description ~= ""
|
||||
end
|
||||
|
||||
-- Builds a target table for a dropped-item entity, or returns nil if not applicable.
|
||||
local function get_dropped_item_target(ref, entity)
|
||||
local stack = get_entity_itemstack(entity)
|
||||
|
||||
if not stack then
|
||||
return nil
|
||||
end
|
||||
|
||||
local item_name = stack:get_name()
|
||||
|
||||
if item_name == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local item_def = minetest.registered_items[item_name] or {}
|
||||
local entity_pos = ref and ref:get_pos() or { x = 0, y = 0, z = 0 }
|
||||
local count = stack:get_count() or 1
|
||||
local meta_summary, has_meta, has_custom_description = summarize_item_meta(stack)
|
||||
local custom_description = has_custom_description and first_line(stack:get_meta():get_string("description")) or nil
|
||||
local stack_description = nil
|
||||
|
||||
if stack.get_short_description then
|
||||
stack_description = first_line(stack:get_short_description())
|
||||
end
|
||||
|
||||
if not stack_description and stack.get_description then
|
||||
stack_description = first_line(stack:get_description())
|
||||
end
|
||||
|
||||
local base_description = custom_description
|
||||
or stack_description
|
||||
or first_line(item_def.short_description)
|
||||
or first_line(item_def.description)
|
||||
or eye_spy.readable_name_from_id(item_name)
|
||||
or S("Unknown")
|
||||
local description = count > 1 and (base_description .. " x" .. count) or base_description
|
||||
|
||||
return {
|
||||
kind = "item",
|
||||
key = "item:" .. item_name .. ":" .. count .. ":" .. tostring(ref),
|
||||
name = item_name,
|
||||
full_name = item_name,
|
||||
description = description,
|
||||
modname = eye_spy.get_modname_from_obj(item_name) or S("Unknown"),
|
||||
pos = vector.new(entity_pos),
|
||||
ref = ref,
|
||||
stack = stack,
|
||||
stack_count = count,
|
||||
stack_wear = stack:get_wear() or 0,
|
||||
stack_has_meta = has_meta,
|
||||
stack_meta_summary = meta_summary,
|
||||
stack_has_custom_description = has_custom_description,
|
||||
texture = extract_item_texture(item_name, item_def),
|
||||
mesh = nil,
|
||||
light_level = nil,
|
||||
item_def_description = item_def.short_description or item_def.description or "",
|
||||
}
|
||||
end
|
||||
|
||||
-- Returns positions to sample when determining a node's ambient light level.
|
||||
local function get_light_sample_positions(pos, def)
|
||||
local positions = {
|
||||
{ x = pos.x, y = pos.y, z = pos.z },
|
||||
{ x = pos.x, y = pos.y + 1, z = pos.z },
|
||||
{ x = pos.x + 1, y = pos.y, z = pos.z },
|
||||
{ x = pos.x - 1, y = pos.y, z = pos.z },
|
||||
{ x = pos.x, y = pos.y, z = pos.z + 1 },
|
||||
{ x = pos.x, y = pos.y, z = pos.z - 1 },
|
||||
}
|
||||
|
||||
-- For non-walkable nodes also sample below (e.g. torches, plants).
|
||||
if def and not def.walkable then
|
||||
positions[#positions + 1] = { x = pos.x, y = pos.y - 1, z = pos.z }
|
||||
end
|
||||
|
||||
return positions
|
||||
end
|
||||
|
||||
-- Microsecond TTL for the light-sample cache.
|
||||
local LIGHT_CACHE_TTL_US = 200000
|
||||
local light_sample_cache = {}
|
||||
local light_sample_cache_size = 0
|
||||
|
||||
-- Returns the highest light value among all sampled neighbour positions.
|
||||
local function get_best_node_light(pos, def)
|
||||
local best_light
|
||||
|
||||
for _, sample_pos in ipairs(get_light_sample_positions(pos, def)) do
|
||||
local light = minetest.get_node_light(sample_pos)
|
||||
|
||||
if light ~= nil and (best_light == nil or light > best_light) then
|
||||
best_light = light
|
||||
end
|
||||
end
|
||||
|
||||
return best_light
|
||||
end
|
||||
|
||||
-- Cached wrapper around get_best_node_light; evicts the cache when it grows
|
||||
-- beyond 1 024 entries.
|
||||
local function get_cached_node_light(pos, def)
|
||||
local now_us = minetest.get_us_time and minetest.get_us_time() or 0
|
||||
|
||||
if now_us <= 0 then
|
||||
return get_best_node_light(pos, def)
|
||||
end
|
||||
|
||||
local cache_key = pos_key(pos) .. ":" .. ((def and def.walkable) and "w" or "nw")
|
||||
local cached = light_sample_cache[cache_key]
|
||||
|
||||
if cached and (now_us - cached.time_us) <= LIGHT_CACHE_TTL_US then
|
||||
return cached.value
|
||||
end
|
||||
|
||||
local sampled = get_best_node_light(pos, def)
|
||||
|
||||
if light_sample_cache_size > 1024 then
|
||||
light_sample_cache = {}
|
||||
light_sample_cache_size = 0
|
||||
end
|
||||
|
||||
if not light_sample_cache[cache_key] then
|
||||
light_sample_cache_size = light_sample_cache_size + 1
|
||||
end
|
||||
|
||||
light_sample_cache[cache_key] = {
|
||||
value = sampled,
|
||||
time_us = now_us,
|
||||
}
|
||||
|
||||
return sampled
|
||||
end
|
||||
|
||||
-- Returns the current HP of an entity, preferring the luaentity field over the
|
||||
-- object method.
|
||||
local function get_entity_hp(ref, entity)
|
||||
local hp = entity and (entity.health or entity.hp)
|
||||
|
||||
if hp == nil and ref then
|
||||
hp = ref:get_hp()
|
||||
end
|
||||
|
||||
return hp
|
||||
end
|
||||
|
||||
-- Fix 3: Returns the maximum HP of an entity.
|
||||
-- Checks luaentity fields first, then the Minetest 5.8+ get_max_hp() API,
|
||||
-- and only falls back to current HP as a last resort.
|
||||
local function get_entity_max_hp(ref, entity)
|
||||
if entity then
|
||||
local m = entity.max_health or entity.max_hp or entity.hp_max
|
||||
if m then return m end
|
||||
end
|
||||
|
||||
-- Use get_max_hp() if available (Minetest 5.8+)
|
||||
if ref and ref.get_max_hp then
|
||||
local max_hp = ref:get_max_hp()
|
||||
if max_hp and max_hp > 0 then return max_hp end
|
||||
end
|
||||
|
||||
if ref then
|
||||
return ref:get_hp() -- last resort: current HP
|
||||
end
|
||||
end
|
||||
|
||||
-- Fix 4: Returns a synthetic target table used for UI preview rendering.
|
||||
-- When preview_type == "Node" a node target is returned; otherwise an entity
|
||||
-- target is returned (this was previously inverted).
|
||||
function eye_spy.target.get_preview(preview_type)
|
||||
if preview_type == "Node" then
|
||||
local def = minetest.registered_nodes["eye_spy:preview_node"]
|
||||
|
||||
return {
|
||||
kind = "node",
|
||||
key = "preview:node",
|
||||
name = "eye_spy:preview_node",
|
||||
full_name = "eye_spy:preview_node",
|
||||
description = def and def.description or S("Preview") .. " " .. S("Node"),
|
||||
modname = "eye_spy",
|
||||
pos = { x = 0, y = 0, z = 0 },
|
||||
def = def,
|
||||
texture = def and def.tiles and def.tiles[1] or "eye_spy_default_texture.png",
|
||||
mesh = def and def.mesh or "eye_spy_default_mesh.obj",
|
||||
color = def and def.color,
|
||||
palette = def and def.palette,
|
||||
light_level = 15,
|
||||
is_liquid = false,
|
||||
}
|
||||
end
|
||||
|
||||
-- Default (including "Entity"): return an entity preview target.
|
||||
local entity_def = minetest.registered_entities["eye_spy:preview_entity"]
|
||||
local props = entity_def and entity_def.initial_properties or {}
|
||||
|
||||
return {
|
||||
kind = "entity",
|
||||
key = "preview:entity",
|
||||
name = "eye_spy:preview_entity",
|
||||
full_name = "eye_spy:preview_entity",
|
||||
description = props.description or S("Preview") .. " " .. S("Entity"),
|
||||
modname = "eye_spy",
|
||||
pos = { x = 0, y = 0, z = 0 },
|
||||
texture = props.textures and props.textures[1] or "eye_spy_default_texture.png",
|
||||
mesh = props.mesh or "eye_spy_default_mesh.obj",
|
||||
hp = props.health or props.hp or 8,
|
||||
max_hp = props.max_health or props.max_hp or props.hp_max or 10,
|
||||
light_level = 15,
|
||||
}
|
||||
end
|
||||
|
||||
-- Casts a ray from the player's eye and returns a target table describing
|
||||
-- whatever the player is looking at (node, entity, dropped item, or air).
|
||||
function eye_spy.target.acquire(player, opts)
|
||||
local options = opts or {}
|
||||
|
||||
if options.preview then
|
||||
return eye_spy.target.get_preview(options.preview_type)
|
||||
end
|
||||
|
||||
local pos = player:get_pos()
|
||||
local meta = player and player:get_meta() or nil
|
||||
|
||||
-- Fix 1: (meta and X) or true is always true in Lua.
|
||||
-- Correct form: absent meta means show everything; present meta is checked.
|
||||
local show_light_level = (not meta) or (meta:get_string("es_show_light_level") ~= "false")
|
||||
local show_spawn_hint = (not meta) or (meta:get_string("es_show_spawn_hint") ~= "false")
|
||||
local show_liquid_info = (not meta) or (meta:get_string("es_show_liquid_info") ~= "false")
|
||||
|
||||
local need_node_light = show_light_level or show_spawn_hint
|
||||
|
||||
local properties = player:get_properties()
|
||||
local eye_height = properties.eye_height or 1.625
|
||||
|
||||
-- Fix 5: delegate range calculation to the shared helper in tools.lua.
|
||||
local range = eye_spy.get_player_range(player)
|
||||
|
||||
pos.y = pos.y + eye_height
|
||||
|
||||
local dir = player:get_look_dir()
|
||||
local start = vector.add(pos, vector.multiply(dir, 0.2))
|
||||
local finish = vector.add(start, vector.multiply(dir, range))
|
||||
local allow_liquids = eye_spy.config.show_liquids == true and show_liquid_info
|
||||
local ray = minetest.raycast(start, finish, eye_spy.config.show_entities, allow_liquids)
|
||||
local first_liquid = nil
|
||||
local primary_target = nil
|
||||
|
||||
-- Builds a complete node target table.
|
||||
local function make_node_target(node, under_pos, def, is_liquid, liquid_type, liquid_level)
|
||||
return {
|
||||
kind = "node",
|
||||
key = "node:" .. node.name .. "@" .. pos_key(under_pos),
|
||||
name = node.name,
|
||||
full_name = node.name,
|
||||
description = first_line(def and def.description)
|
||||
or eye_spy.readable_name_from_id(node.name)
|
||||
or S("Unknown"),
|
||||
modname = eye_spy.get_modname_from_obj(node.name) or S("Unknown"),
|
||||
pos = vector.new(under_pos),
|
||||
def = def,
|
||||
texture = extract_node_texture(def),
|
||||
mesh = def and def.mesh or "eye_spy_default_mesh.obj",
|
||||
color = def and def.color,
|
||||
palette = def and def.palette,
|
||||
light_level = need_node_light and get_cached_node_light(under_pos, def) or nil,
|
||||
is_liquid = is_liquid,
|
||||
liquid_type = liquid_type,
|
||||
liquid_level = liquid_level,
|
||||
behind_liquid = false,
|
||||
liquid_obstacle = nil,
|
||||
}
|
||||
end
|
||||
|
||||
for hit in ray do
|
||||
if hit.type == "node" then
|
||||
local node = minetest.get_node(hit.under)
|
||||
|
||||
if node.name ~= "air" then
|
||||
local def = minetest.registered_nodes[node.name]
|
||||
local liquid_type = def and def.liquidtype or "none"
|
||||
local is_liquid = liquid_type ~= "none"
|
||||
local liquid_level
|
||||
|
||||
if is_liquid then
|
||||
if liquid_type == "source" then
|
||||
liquid_level = 8
|
||||
else
|
||||
liquid_level = ((node.param2 or 0) % 8) + 1
|
||||
end
|
||||
|
||||
if not first_liquid then
|
||||
first_liquid = {
|
||||
name = node.name,
|
||||
full_name = node.name,
|
||||
pos = vector.new(hit.under),
|
||||
liquid_type = liquid_type,
|
||||
liquid_level = liquid_level,
|
||||
}
|
||||
end
|
||||
else
|
||||
primary_target = make_node_target(node, hit.under, def, false, liquid_type, liquid_level)
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif hit.type == "object" and hit.ref and not hit.ref:is_player() then
|
||||
local entity = hit.ref:get_luaentity()
|
||||
local dropped_item = get_dropped_item_target(hit.ref, entity)
|
||||
|
||||
if dropped_item then
|
||||
dropped_item.behind_liquid = false
|
||||
dropped_item.liquid_obstacle = nil
|
||||
primary_target = dropped_item
|
||||
break
|
||||
end
|
||||
|
||||
local entity_name = entity and entity.name or "unknown"
|
||||
local entity_pos = hit.ref:get_pos() or { x = 0, y = 0, z = 0 }
|
||||
|
||||
primary_target = {
|
||||
kind = "entity",
|
||||
key = "entity:" .. entity_name .. ":" .. tostring(hit.ref),
|
||||
name = entity_name,
|
||||
full_name = entity_name,
|
||||
description = first_line(entity and entity.description)
|
||||
or eye_spy.readable_name_from_id(entity_name)
|
||||
or S("Unknown"),
|
||||
modname = eye_spy.get_modname_from_obj(entity_name) or S("Unknown"),
|
||||
pos = vector.new(entity_pos),
|
||||
ref = hit.ref,
|
||||
texture = extract_entity_texture(entity),
|
||||
mesh = entity and entity.mesh or "eye_spy_default_mesh.obj",
|
||||
hp = get_entity_hp(hit.ref, entity),
|
||||
max_hp = get_entity_max_hp(hit.ref, entity),
|
||||
light_level = nil,
|
||||
behind_liquid = false,
|
||||
liquid_obstacle = nil,
|
||||
}
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if primary_target then
|
||||
if first_liquid then
|
||||
primary_target.behind_liquid = true
|
||||
primary_target.liquid_obstacle = first_liquid
|
||||
end
|
||||
|
||||
return primary_target
|
||||
end
|
||||
|
||||
-- No solid target found — return the liquid itself if one was encountered.
|
||||
if first_liquid then
|
||||
local node_name = first_liquid.name
|
||||
local node_pos = first_liquid.pos
|
||||
local def = minetest.registered_nodes[node_name]
|
||||
|
||||
return {
|
||||
kind = "node",
|
||||
key = "node:" .. node_name .. "@" .. pos_key(node_pos),
|
||||
name = node_name,
|
||||
full_name = node_name,
|
||||
description = first_line(def and def.description)
|
||||
or eye_spy.readable_name_from_id(node_name)
|
||||
or S("Unknown"),
|
||||
modname = eye_spy.get_modname_from_obj(node_name) or S("Unknown"),
|
||||
pos = vector.new(node_pos),
|
||||
def = def,
|
||||
texture = extract_node_texture(def),
|
||||
mesh = def and def.mesh or "eye_spy_default_mesh.obj",
|
||||
color = def and def.color,
|
||||
palette = def and def.palette,
|
||||
light_level = need_node_light and get_cached_node_light(node_pos, def) or nil,
|
||||
is_liquid = true,
|
||||
liquid_type = first_liquid.liquid_type,
|
||||
liquid_level = first_liquid.liquid_level,
|
||||
behind_liquid = false,
|
||||
liquid_obstacle = nil,
|
||||
}
|
||||
end
|
||||
|
||||
return { kind = "air", key = "air", behind_liquid = false, liquid_obstacle = nil }
|
||||
end
|
||||
|
After Width: | Height: | Size: 220 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 172 B |
|
After Width: | Height: | Size: 176 B |
|
After Width: | Height: | Size: 100 B |
@@ -0,0 +1,282 @@
|
||||
-- eye_spy/tools.lua
|
||||
-- Tool detection and dig-hint helpers.
|
||||
-- Extracted from functions.lua. Runs after functions.lua, so eye_spy.S and
|
||||
-- eye_spy.cg are already available when this file is executed.
|
||||
|
||||
local S = eye_spy.S
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Group → tool-type mappings for Minetest Game
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Maps dig group names to human-readable tool-type labels (MTG).
|
||||
local GROUP_TO_TOOL_MTG = {
|
||||
cracky = S("Pickaxe"),
|
||||
crumbly = S("Shovel"),
|
||||
choppy = S("Axe"),
|
||||
snappy = S("Sword"),
|
||||
dig_immediate = S("Hand"),
|
||||
}
|
||||
|
||||
-- Priority order for MTG group lookup (most-specific first).
|
||||
local GROUP_ORDER_MTG = { "dig_immediate", "snappy", "crumbly", "choppy", "cracky" }
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Group → tool-type mappings for MineClone / VoxeLibre
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Maps dig group names to human-readable tool-type labels (MCL).
|
||||
local GROUP_TO_TOOL_MC = {
|
||||
swordy = S("Sword"),
|
||||
handy = S("Hand"),
|
||||
pickaxey = S("Pickaxe"),
|
||||
shovely = S("Shovel"),
|
||||
axey = S("Axe"),
|
||||
shearsy = S("Shears"),
|
||||
dig_immediate = S("Hand"),
|
||||
}
|
||||
|
||||
-- Priority order for MCL group lookup (most-specific first).
|
||||
local GROUP_ORDER_MC = { "dig_immediate", "handy", "swordy", "shearsy", "shovely", "axey", "pickaxey" }
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Internal helpers
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Returns a shallow copy of an ipairs-iterable list.
|
||||
local function clone_list(values)
|
||||
local result = {}
|
||||
|
||||
for index, value in ipairs(values or {}) do
|
||||
result[index] = value
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Appends `value` to `list` only when it is not already present.
|
||||
-- Ignores nil and empty-string values.
|
||||
local function append_unique(list, value)
|
||||
if not value or value == "" then
|
||||
return
|
||||
end
|
||||
|
||||
for _, existing in ipairs(list) do
|
||||
if existing == value then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
list[#list + 1] = value
|
||||
end
|
||||
|
||||
-- Returns the group→tool map and ordered group list appropriate for the
|
||||
-- currently-running game. Uses eye_spy.cg (set by init.lua).
|
||||
local function get_tool_mapping(game_id)
|
||||
if game_id == "mineclone" then
|
||||
return GROUP_TO_TOOL_MC, GROUP_ORDER_MC
|
||||
end
|
||||
|
||||
return GROUP_TO_TOOL_MTG, GROUP_ORDER_MTG
|
||||
end
|
||||
|
||||
-- Collects every tool-type label that applies to `node_groups`, in priority
|
||||
-- order, de-duplicated.
|
||||
local function collect_tool_types(node_groups, group_to_tool, group_order)
|
||||
local result = {}
|
||||
|
||||
for _, group in ipairs(group_order) do
|
||||
if node_groups[group] and group_to_tool[group] then
|
||||
append_unique(result, group_to_tool[group])
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Returns the single highest-priority dig group and its tool-type label for
|
||||
-- `node_groups`. Returns nil, S("None") when no matching group is found.
|
||||
local function get_preferred_tool_group(node_groups, group_to_tool, group_order)
|
||||
for _, group in ipairs(group_order) do
|
||||
if node_groups[group] and group_to_tool[group] then
|
||||
return group, group_to_tool[group]
|
||||
end
|
||||
end
|
||||
|
||||
return nil, S("None")
|
||||
end
|
||||
|
||||
-- Builds a display string from the full set of required tool types and the
|
||||
-- single best/preferred tool type.
|
||||
--
|
||||
-- Examples:
|
||||
-- { "Pickaxe" }, "Pickaxe" → "Pickaxe"
|
||||
-- { "Axe", "Pickaxe" }, "Pickaxe" → "Axe / Pickaxe | Pickaxe"
|
||||
local function format_tool_summary(minimum_types, best_type)
|
||||
local minimum_label = table.concat(minimum_types or {}, " / ")
|
||||
|
||||
if best_type and best_type ~= "" and best_type ~= S("None") and best_type ~= S("Unknown") then
|
||||
if minimum_label == "" then
|
||||
return best_type
|
||||
end
|
||||
|
||||
-- Single entry that matches best_type exactly — no need to repeat it.
|
||||
if #minimum_types == 1 and minimum_types[1] == best_type then
|
||||
return best_type
|
||||
end
|
||||
|
||||
-- If best_type is already listed in minimum_types, just show the list.
|
||||
for _, minimum_type in ipairs(minimum_types or {}) do
|
||||
if minimum_type == best_type then
|
||||
return minimum_label
|
||||
end
|
||||
end
|
||||
|
||||
-- best_type is not in the minimum list — append it after a separator.
|
||||
return minimum_label .. " | " .. best_type
|
||||
end
|
||||
|
||||
if minimum_label ~= "" then
|
||||
return minimum_label
|
||||
end
|
||||
|
||||
return S("Unknown")
|
||||
end
|
||||
|
||||
-- Returns true when `itemdef` has groupcaps for `preferred_group`.
|
||||
-- dig_immediate is never considered a "match" because any item can break those
|
||||
-- nodes instantly.
|
||||
local function tool_matches_group(itemdef, preferred_group)
|
||||
if not preferred_group or preferred_group == "dig_immediate" then
|
||||
return false
|
||||
end
|
||||
|
||||
local caps = itemdef and itemdef.tool_capabilities
|
||||
local groupcaps = caps and caps.groupcaps
|
||||
|
||||
return groupcaps and groupcaps[preferred_group] ~= nil or false
|
||||
end
|
||||
|
||||
------------------------------------------------------------
|
||||
-- Public API
|
||||
------------------------------------------------------------
|
||||
|
||||
-- Returns whether `toolstack` can dig `nodename`, plus the dig time.
|
||||
-- Returns false, nil when `toolstack` is nil or the node is unknown.
|
||||
function eye_spy.tool_can_dig_node(toolstack, nodename)
|
||||
-- Nil-guard: no stack means no digging ability.
|
||||
if not toolstack then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local ndef = minetest.registered_nodes[nodename]
|
||||
|
||||
if not ndef then
|
||||
return false, nil
|
||||
end
|
||||
|
||||
local tool_caps = toolstack:get_tool_capabilities()
|
||||
local node_groups = ndef.groups or {}
|
||||
local dig_params = minetest.get_dig_params(node_groups, tool_caps)
|
||||
|
||||
return dig_params.diggable, dig_params.time
|
||||
end
|
||||
|
||||
-- Returns a filtered copy of `source` that omits every entry whose `.name`
|
||||
-- field contains any substring listed in `blocked`.
|
||||
function eye_spy.filter_out_by_name(source, blocked)
|
||||
local result = {}
|
||||
|
||||
for _, entry in ipairs(source) do
|
||||
local name = entry.name or ""
|
||||
local keep = true
|
||||
|
||||
for _, word in ipairs(blocked) do
|
||||
if string.find(name, word, 1, true) then
|
||||
keep = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if keep then
|
||||
table.insert(result, entry)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
-- Returns the highest `maxlevel` value found across all groupcaps of
|
||||
-- `itemdef`. Returns 0 when `itemdef` has no tool_capabilities.
|
||||
function eye_spy.get_tool_maxlevel(itemdef)
|
||||
local maxlevel = 0
|
||||
|
||||
if not itemdef.tool_capabilities then
|
||||
return 0
|
||||
end
|
||||
|
||||
for _, gc in pairs(itemdef.tool_capabilities.groupcaps or {}) do
|
||||
if gc.maxlevel and gc.maxlevel > maxlevel then
|
||||
maxlevel = gc.maxlevel
|
||||
end
|
||||
end
|
||||
|
||||
return maxlevel
|
||||
end
|
||||
|
||||
-- Returns a human-readable tool-hint string for `node_name` (or nil when no
|
||||
-- hint is applicable) and a boolean indicating whether the currently wielded
|
||||
-- `toolstack` is an appropriate tool for digging that node.
|
||||
function eye_spy.get_node_tool_hint_and_match(node_name, toolstack)
|
||||
if not node_name or node_name == "" then
|
||||
return nil, false
|
||||
end
|
||||
|
||||
local def = minetest.registered_nodes[node_name]
|
||||
|
||||
if not def or not def.groups then
|
||||
return nil, false
|
||||
end
|
||||
|
||||
local node_groups = def.groups
|
||||
local group_to_tool, group_order = get_tool_mapping(eye_spy.cg)
|
||||
local minimum_types = collect_tool_types(node_groups, group_to_tool, group_order)
|
||||
local preferred_group, tooltype = get_preferred_tool_group(node_groups, group_to_tool, group_order)
|
||||
local summary = format_tool_summary(minimum_types, tooltype)
|
||||
|
||||
-- Suppress generic/unhelpful labels.
|
||||
if summary == S("Unknown") or summary == S("None") then
|
||||
summary = nil
|
||||
end
|
||||
|
||||
-- Nodes with no preferred dig group (or dig_immediate) are considered
|
||||
-- compatible with any held item for colour-state purposes.
|
||||
if not preferred_group or preferred_group == "dig_immediate" then
|
||||
return summary, true
|
||||
end
|
||||
|
||||
local itemdef = toolstack and toolstack.get_definition and toolstack:get_definition() or nil
|
||||
local caps = itemdef and itemdef.tool_capabilities or nil
|
||||
local groupcaps = caps and caps.groupcaps or nil
|
||||
local matches = groupcaps and groupcaps[preferred_group] ~= nil or false
|
||||
|
||||
return summary, matches
|
||||
end
|
||||
|
||||
-- Returns the effective reach range for `player` based on the wielded item
|
||||
-- definition, falling back to creative-mode range (10) or default range (4).
|
||||
function eye_spy.get_player_range(player)
|
||||
local wield = player:get_wielded_item()
|
||||
local def = wield:get_definition()
|
||||
|
||||
-- Nil-guard: definition may be absent for unknown item types.
|
||||
if def and def.range then
|
||||
return def.range
|
||||
end
|
||||
|
||||
if minetest.is_creative_enabled(player:get_player_name()) then
|
||||
return 10
|
||||
end
|
||||
|
||||
return 4
|
||||
end
|
||||