update craft guide (resolves error messages). add coins mod (obsoletes commoditymarket). remove commoditymarket for simplicity and lack of use. test remove hud text on npc-traders.

master
FreeGamers 2020-05-28 02:47:39 -05:00
parent 647762e0dc
commit 2b590b31ff
74 changed files with 1190 additions and 4960 deletions

View File

@ -1,7 +1,8 @@
Sara's Simple Survival Game
==========================
Copyright (C) 2019-2020 FreeGamers.org
Thank you to everyone who has contributed to this project, mods, and the FreeGamers communitys.
Thank you to everyone who has contributed to this project, mods, and the FreeGamers community.
Special thanks to the Minetest community and developers as well. This game was built using the Minetest game engine and upon minetest_game as a foundation.
Description
-------------------------

10
guides/server-hosting.txt Normal file
View File

@ -0,0 +1,10 @@
How to host a dedicated server.
server hardware
domain name or dynamic dns
port forwards and firewalls
operating system.
how to install
administration fundamentals
How to host a listen (local LAN) server.

View File

@ -16,13 +16,6 @@ initial_stuff = default:pick_wood,default:axe_wood,default:sword_wood,default:to
#################
# MOD CONFIGS #
#################
# commoditymarket
commoditymarket_enable_kings_market = true
commoditymarket_enable_night_market = true
commoditymarket_enable_caravan_market = true
commoditymarket_enable_goblin_market = true
commoditymarket_enable_under_market = true
commoditymarket_coins_per_ingot = 100
# craftguide
craftguide_sfinv_only = true

674
mods/coins/LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

17
mods/coins/Readme.txt Normal file
View File

@ -0,0 +1,17 @@
saras_simple_survival mod: coins
==============================
Copyright (2020) freegamers.org
License of source code
------------------------------
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
https://www.gnu.org/licenses/gpl-3.0.html
License of textures
-------------------
commoditymarket_gold_coins.png - from https://commons.wikimedia.org/wiki/File:Farm-Fresh_coins.png, by FatCow under the CC-BY 3.0 license

7
mods/coins/crafting.lua Normal file
View File

@ -0,0 +1,7 @@
-- Cook gold ingots into 100 gold coins.
minetest.register_craft({
type = "cooking",
cooktime = "60",
output = "coins:gold_coins 100",
recipe = "default:gold_ingot",
})

14
mods/coins/init.lua Normal file
View File

@ -0,0 +1,14 @@
-- TODO: coin press machine. copper, silver, coins. melt coins back to ingots
local path = minetest.get_modpath("coins")
dofile(path .. "/crafting.lua")
-- This number controls the number of coins per ingot.
local coins_per_ingot = 100
-- Register the gold coin craft item.
minetest.register_craftitem("coins:gold_coins", {
description = "Gold Coins",
inventory_image = "coins_gold_coins.png",
stack_max = coins_per_ingot,
})

6
mods/coins/mod.conf Normal file
View File

@ -0,0 +1,6 @@
name = coins
author = FreeGamers.org
description = Ore-based currencies.
depends = default

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -1,40 +0,0 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 FaceDeer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,41 +0,0 @@
if not minetest.get_modpath("doc") then
return
end
-- internationalization boilerplate
local MP = minetest.get_modpath(minetest.get_current_modname())
local S, NS = dofile(MP.."/intllib.lua")
doc.add_category("commoditymarket",
{
name = S("Commodity Markets"),
description = S("Game-wide marketplaces where goods can be bought and sold at prices of your choice."),
build_formspec = doc.entry_builders.text_and_gallery,
})
doc.add_entry("commoditymarket", "ui_inventory", {
name = S("User Interface: Inventory"),
data = { text =
S("Each player's account has an inventory that serves as a holding area for items that are destined to be sold or that have been bought by the player but not yet retrieved. This inventory is a bit different from the standard Minetest inventory in that it doesn't hold item \"stacks\", it just tracks the total number of that item present. Some markets allow for extremely large quantities of an item to be stored here for sale."
.."\n\n"..
"To add an item to your market inventory for eventual sale either shift-click on the item in your player inventory or drag the item stack to the inventory slot below the main market inventory list. Some markets may have restrictions on what items can be bought and sold, if an item is not valid for that market it won't go into the market's inventory. Some items are considered \"currency\" and will add to your account's currency balance instead of being listed in your market inventory."
.."\n\n"..
"Tools cannot be added to the market inventory if they have any wear on them. The market also can't handle items with attached metadata such as books that have had text added to them."
.."\n\n"..
"To remove an item from your market inventory, double-click in it in the market inventory list. As much of the item as can fit into your player inventory will be transferred to you, with any remainder staying behind in the market inventory. To withdraw currency from your market balance type the amount you'd like to withdraw in the field next to the \"Withdraw\" button. The currency will be converted into items and added to your player inventory, with whatever cannot be converted remaining behind in your market balance.")
}})
doc.add_entry("commoditymarket", "ui_orders", {
name = S("User Interface: Orders"),
data = { text =
S(
"At the core of how a market operates are \"buy\" and \"sell\" orders. A buy order is an announcement to the world that you are interested in purchasing a certain quantity of item and are willing to pay a certain amount of currency in exchange for each unit of that item. Conversely, a sell order is an announcement to the world that you are interested in selling a certain quantity of item and will accept a certain amount of currency in exchange for each unit of that item."
.."\n\n"..
"The market price of an item is determined by where the existing buy and sell orders for that item intersect. When you offer to buy an item for a price that someone is offering to sell it at, the item is transferred to you and currency is transferred from your account to theirs to cover the cost. The market will keep track of the most recent price that an item was successfully sold for, but note that this information is for historical interest only - there's no guarantee that anyone is currently willing to match the historical price."
.."\n\n"..
"When an item is selected in the upper list, the currently existing buy and sell orders for that item will be displayed in the lower list. Sell orders are listed first in descending price, followed by buy orders in ascending price. The current market price will be somewhere in between the lowest sell order and the highest buy order. If you wish to cancel a buy or sell order that you've placed for an item, double-click on the order and the item or currency that you put into that order will be returned to your inventory."
.."\n\n"..
"If you place a buy order and there are already sell orders for the item that meet or are below your price, some or all of your buy order might be immediately fulfilled. Your purchases will be made at the price that the sell orders have been set to - if you were willing to pay 15 units of currency per item but someone was already offering to sell for 2 units of currency per item, you only pay 2 units for each of that offer's items. If there aren't enough compatible sell orders to fulfill your buy order, the remainder will be placed into the market and made available for future sellers to see and fulfill if they agree to your price. Your buy order will immediately deduct the currency required for it from your account's balance, but if you cancel your order you will get that currency back - it's not gone until the order is actually fulfilled."
.."\n\n"..
"If you place a sell order and there are already buy orders that meet or exceed your price, some or all of your sell order may be immediately fulfilled. You'll be paid the price that the buyers are offering rather than the amount you're demanding. If any of your sell offer is left unfulfilled, the sell order will be added to the market for future buyers to see. The items for this offer will be immediately taken from your market inventory but if you cancel your order you will get those items back.")
}})

View File

@ -1,790 +0,0 @@
local S = minetest.get_translator(minetest.get_current_modname())
local truncate_item_names_to = 30
-- Large textures can screw with the formspecs.
-- See https://github.com/minetest/minetest/issues/9300 for a feature request that would simplify and improve icon generation, if supported.
-- In the meantime, here's some methods for overriding item icons to manually work around this:
local override_item_icon = {}
commoditymarket.override_item_icon = function(item_name, new_icon_texture)
override_item_icon[item_name] = new_icon_texture
end
local override_image_icon = {}
commoditymarket.override_image_icon = function(old_icon_texture, new_icon_texture)
override_image_icon[old_icon_texture] = new_icon_texture
end
-- And a setting for disabling icons entirely:
local global_enable_item_icons = minetest.settings:get_bool("commoditymarket_enable_item_icons", true)
--[inventorycube{<top>{<left>{<right>
--Escaping does not apply here and `^` is replaced by `&` in texture names instead.
--Example:
-- [inventorycube{grass.png{dirt.png&grass_side.png{dirt.png&grass_side.png
--Creates an inventorycube with `grass.png`, `dirt.png^grass_side.png` and `dirt.png^grass_side.png` textures
local process_inventory_cube = function(texture_string)
if not texture_string:sub(1,14) == "[inventorycube" then
return texture_string
end
local split = texture_string:split("{")
local left = split[3] -- the "front" of the cube we're seeing in the inventory list
if left == nil then -- in case something weird happens, don't crash.
return texture_string
end
left = left:gsub("&", "^")
return left
end
local get_icon = function(item)
local def = minetest.registered_items[item]
local returnstring = "unknown_item.png"
if def == nil then
return returnstring
end
local override = override_item_icon[item]
if override then
return override
end
local inventory_image = def.inventory_image
if inventory_image and inventory_image ~= "" then
returnstring = inventory_image
else
local tiles = def.tiles
if tiles then
local tilecount = #tiles
-- Textures of node; +Y, -Y, +X, -X, +Z, -Z
local selected_tile = tiles[math.min(5,tilecount)]
if type(selected_tile) == "string" then
returnstring = selected_tile
else
local tile_name = selected_tile.name
if tile_name then
returnstring = tile_name
end
end
end
end
returnstring = process_inventory_cube(returnstring)
-- Formspec tables can't handle image compositing and modifiers
local found_caret = returnstring:find("%^")
if found_caret then
returnstring = returnstring:sub(1, found_caret-1)
end
override = override_image_icon[returnstring]
if override then
return override
end
return minetest.formspec_escape(returnstring)
end
-- Exposed so that the purge_unknowns command can use it.
commoditymarket.get_icon = get_icon
local truncate_string = function(target, length)
if target:len() > length then
return target:sub(1,length-2).."..."
end
return target
end
local get_item_description = function(item)
local def = minetest.registered_items[item]
if def then
local description = def.description
if description then
return minetest.formspec_escape(description:gsub("\n", " "))
end
end
return S("Unknown Item")
end
-- Inventory formspec
-------------------------------------------------------------------------------------
local inventory_item_comp = function(invitem1, invitem2) return invitem1.item < invitem2.item end
local inventory_desc_comp = function(invitem1, invitem2) return invitem1.description < invitem2.description end
local get_account_formspec = function(market, account)
local show_itemnames = account.show_itemnames == "true"
local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
local market_def = market.def
local inventory = {}
local inventory_count = 0
for item, quantity in pairs(account.inventory) do
local icon
if show_icons then
icon = get_icon(item)
end
table.insert(inventory, {item=item, quantity=quantity, icon=icon, description=get_item_description(item)})
inventory_count = inventory_count + quantity
end
if show_itemnames then
table.sort(inventory, inventory_item_comp)
else
table.sort(inventory, inventory_desc_comp)
end
local formspec = {
"size[10,10]"
.."tabheader[0,0;tabs;"..market_def.description..","..S("Your Inventory")..","..S("Market Orders")..";2;false;true]"
}
formspec[#formspec+1] = "tablecolumns["
if show_icons then
formspec[#formspec+1] = "image"
for i=1, #inventory, 2 do
formspec[#formspec+1] = ","..i.."="..inventory[i].icon
end
formspec[#formspec+1] = ";"
end
if show_itemnames then
formspec[#formspec+1] = "text;"
end
formspec[#formspec+1] = "text;text,align=center"
if show_icons then
formspec[#formspec+1] = ";image"
for i=2, #inventory, 2 do
formspec[#formspec+1] = ","..i.."="..inventory[i].icon
end
end
if show_itemnames then
formspec[#formspec+1] = ";text"
end
formspec[#formspec+1] = ";text;text,align=center]"
.."tooltip[inventory;"..S("All the items you've transfered to the market to sell and the items you've\npurchased with buy orders. Double-click on an item to bring it back into your\npersonal inventory.").."]"
.."table[0,0;9.75,4;inventory;"
if show_icons then
formspec[#formspec+1] = "0,"
end
if show_itemnames then
formspec[#formspec+1] = S("Item")..","
end
formspec[#formspec+1] = S("Description")..","..S("Quantity")
if show_icons then
formspec[#formspec+1] = ",0"
end
if show_itemnames then
formspec[#formspec+1] = ","..S("Item")
end
formspec[#formspec+1] = ","..S("Description")..","..S("Quantity")
for i, entry in ipairs(inventory) do
if show_icons then
formspec[#formspec+1] = "," .. i
end
if show_itemnames then
formspec[#formspec+1] = "," .. truncate_string(entry.item, truncate_item_names_to)
end
-- no need to formspec_escape description here, it gets done when it's initially added to the inventory table
formspec[#formspec+1] = "," .. entry.description .. "," .. entry.quantity
end
formspec[#formspec+1] = "]container[1,4.5]list[detached:commoditymarket:" .. market.name .. ";add;0,0;1,1;]"
.."label[1,0;"..S("Drop items here to\nadd to your account").."]"
.."listring[current_player;main]listring[detached:commoditymarket:" .. market.name .. ";add]"
if market_def.inventory_limit then
formspec[#formspec+1] = "label[3,0;"..S("Inventory limit:").."\n" .. inventory_count.."/" .. market_def.inventory_limit .. "]"
.. "tooltip[3,0;1.5,1;"..S("You can still receive purchased items if you've exceeded your inventory limit,\nbut you won't be able to transfer items from your personal inventory into\nthe market until you've emptied it back down below the limit again.").."]"
end
formspec[#formspec+1] = "label[4.9,0;"..S("Balance:") .. "\n" .. market_def.currency_symbol .. account.balance .. "]"
.."tooltip[4.9,0;3.5,1;"..S("Enter the amount of currency you'd like to withdraw then click the 'Withdraw'\nbutton to convert it into items and transfer it to your personal inventory.").."]"
.."field[6.1,0.325;1,1;withdrawamount;;]"
.."field_close_on_enter[withdrawamount;false]"
.."button[6.7,0;1.2,1;withdraw;"..S("Withdraw").."]"
.."container_end[]"
.."container[1,5.75]list[current_player;main;0,0;8,1;]"
.."list[current_player;main;0,1.25;8,3;8]container_end[]"
return table.concat(formspec)
end
-- Market formspec
--------------------------------------------------------------------------------------------------------
local compare_market_item = function(mkt1, mkt2)
return mkt1.item < mkt2.item
end
local compare_market_desc = function(mkt1, mkt2)
-- TODO: see https://github.com/minetest/minetest/issues/8398 for sorting localized strings
return get_item_description(mkt1.item) < get_item_description(mkt2.item)
end
local compare_buy_volume = function(mkt1, mkt2)
return mkt1.buy_volume > mkt2.buy_volume
end
local compare_buy_max = function(mkt1, mkt2)
return ((mkt1.buy_orders[#mkt1.buy_orders] or {}).price or -2^30) > ((mkt2.buy_orders[#mkt2.buy_orders] or {}).price or -2^30)
end
local compare_sell_volume = function(mkt1, mkt2)
return mkt1.sell_volume > mkt2.sell_volume
end
local compare_sell_min = function(mkt1, mkt2)
return ((mkt1.sell_orders[#mkt1.sell_orders] or {}).price or 2^31) < ((mkt2.sell_orders[#mkt2.sell_orders] or {}).price or 2^31)
end
local compare_last_price = function(mkt1, mkt2)
return (mkt1.last_price or 2^31) < (mkt2.last_price or 2^31)
end
local sort_marketlist = function(item_list, account)
-- I think tonumber is now redundant here, leaving it in in case upgrading a world that has text recorded in this field for an existing player account
local sort_by = tonumber(account.sort_markets_by_column)
if sort_by == nil then return end
local show_itemnames = account.show_itemnames == "true"
local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
local icon_displace = 0
if show_icons then
icon_displace = 1
end
local itemname_displace = 0
if show_itemnames then
itemname_displace = 1
end
-- "Icon,Item,Description,#00FF00,Buy Vol,Buy Max,#FF0000,Sell Vol,Sell Min,Last Price"
if sort_by == 1 + icon_displace and show_itemnames then
table.sort(item_list, compare_market_item)
elseif sort_by == 1 + icon_displace + itemname_displace then
table.sort(item_list, compare_market_desc)
elseif sort_by == 3 + icon_displace + itemname_displace then
table.sort(item_list, compare_buy_volume)
elseif sort_by == 4 + icon_displace + itemname_displace then
table.sort(item_list, compare_buy_max)
elseif sort_by == 6 + icon_displace + itemname_displace then
table.sort(item_list, compare_sell_volume)
elseif sort_by == 7 + icon_displace + itemname_displace then
table.sort(item_list, compare_sell_min)
elseif sort_by == 8 + icon_displace + itemname_displace then
table.sort(item_list, compare_last_price)
elseif sort_by == 9 + icon_displace + itemname_displace then
table.sort(item_list, function(mkt1, mkt2)
-- Define locally so that account is available
return (account.inventory[mkt1.item] or 0) > (account.inventory[mkt2.item] or 0)
end)
end
end
local make_marketlist = function(market, account)
local market_list = {}
local search_filter = account.search or ""
for item, row in pairs(market.orders_for_items) do
if (search_filter == "" or string.find(item, search_filter)) then
if account.filter_participating == "true" then
local found = false
for _, order in ipairs(row.buy_orders) do
if account == order.account then
found = true
break
end
end
if not found then
for _, order in ipairs(row.sell_orders) do
if account == order.account then
found = true
break
end
end
end
if found then
table.insert(market_list, row)
end
else
table.insert(market_list, row)
end
end
end
sort_marketlist(market_list, account)
return market_list
end
local get_account_name = function(target_account, this_account, anonymous)
if anonymous and target_account ~= this_account then
return ""
end
return target_account.name
end
local get_market_formspec = function(market, account)
local market_def = market.def
local selected = account.selected
local market_list = make_marketlist(market, account)
local show_itemnames = account.show_itemnames == "true"
local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
local anonymous = market_def.anonymous
local formspec = {
"size[10,10]"
.."tabheader[0,0;tabs;"..market_def.description..","..S("Your Inventory")..","..S("Market Orders")..";3;false;true]"
}
-- column definitions
formspec[#formspec+1] = "tablecolumns["
if show_icons then
formspec[#formspec+1] = "image" -- icon
for i, row in ipairs(market_list) do
formspec[#formspec+1] = "," .. i .. "=" .. get_icon(row.item)
end
formspec[#formspec+1] = ";"
end if show_itemnames then
formspec[#formspec+1] = "text;" -- itemname
end
formspec[#formspec+1] = "text;" -- description
.."color,span=2;"
.."text,align=right,tooltip="..S("Number of items there's demand for in the market.")..";"
.."text,align=right,tooltip="..S("Maximum price being offered to buy one of these.")..";"
.."color,span=2;"
.."text,align=right,tooltip="..S("Number of items available for sale in the market.")..";"
.."text,align=right,tooltip="..S("Minimum price being demanded to sell one of these.")..";"
.."text,align=right,tooltip="..S("Price paid for one of these the last time one was sold.")..";"
.."text,align=right,tooltip="..S("Quantity of this item that you have in your inventory ready to sell.").."]"
.."table[0,0;9.75,5;summary;"
if show_icons then
formspec[#formspec+1] = "0,"-- icon
end
-- header row
if show_itemnames then
formspec[#formspec+1] = "Item," -- itemname
end
formspec[#formspec+1] = S("Description")..",#00FF00,"..S("Buy Vol")..","..S("Buy Max")
..",#FF0000,"..S("Sell Vol")..","..S("Sell Min")..","..S("Last Price")..","..S("Inventory")
local selected_idx
local selected_row
-- Show list of item market summaries
for i, row in ipairs(market_list) do
if show_icons then
formspec[#formspec+1] = ","..i -- icon
end
if show_itemnames then
formspec[#formspec+1] = "," .. truncate_string(row.item, truncate_item_names_to)
end
formspec[#formspec+1] = "," .. get_item_description(row.item)
.. ",#00FF00,"
.. row.buy_volume
.. "," .. ((row.buy_orders[#row.buy_orders] or {}).price or "-")
.. ",#FF0000,"
.. row.sell_volume
.. "," .. ((row.sell_orders[#row.sell_orders] or {}).price or "-")
.. "," .. (row.last_price or "-")
.. "," .. (account.inventory[row.item] or "-")
-- we happen to be processing the row that matches the item this player has selected. Record that.
if selected == row.item then
selected_row = row
selected_idx = i + 1
end
end
-- a row that's visible is marked as the selected item, so make it selected in the formspec
if selected_row then
formspec[#formspec+1] = ";"..selected_idx
end
formspec[#formspec+1] = "]"
-- search field
formspec[#formspec+1] = "container[2.5,5]field_close_on_enter[search_filter;false]"
.."field[0,0.85;2.5,1;search_filter;;"..minetest.formspec_escape(account.search or "").."]"
.."image_button[2.05,0.65;0.8,0.8;commoditymarket_search.png;apply_search;]"
.."image_button[2.7,0.65;0.8,0.8;commoditymarket_clear.png;clear_search;]"
.."checkbox[1.77,0;filter_participating;"..S("My orders")..";".. account.filter_participating .."]"
.."tooltip[filter_participating;"..S("Select this to show only the markets where you have either a buy or a sell order pending.").."]"
.."tooltip[search_filter;"..S("Enter substring to search item identifiers for.").."]"
.."tooltip[apply_search;"..S("Apply search to outputs.").."]"
.."tooltip[clear_search;"..S("Clear search.").."]"
.."container_end[]"
-- if a visible item market is selected, show the orders for it in detail
if selected_row then
local current_time = minetest.get_gametime()
local desc_display
if show_itemnames then
desc_display = selected
else
local def = minetest.registered_items[selected_row.item] or {description=S("Unknown Item")}
desc_display = minetest.formspec_escape(def.description:gsub("\n", " "))
end
-- player inventory for this item and for currency
formspec[#formspec+1] = "label[0.1,5.1;"..desc_display.."\n"..S("In inventory:").." "
.. tostring(account.inventory[selected] or 0) .."\n"..S("Balance:").." "..market_def.currency_symbol..account.balance .."]"
-- buy/sell controls
.. "container[6.1,5]"
local sell_limit = market_def.sell_limit
if sell_limit then
local total_sell = 0
for item, orders in pairs(market.orders_for_items) do
for _, order in ipairs(orders.sell_orders) do
if order.account == account then
total_sell = total_sell + order.quantity
end
end
end
formspec[#formspec+1] = "label[0,0;"..S("Sell limit:").." ".. total_sell .. "/" .. sell_limit .."]"
.."tooltip[0,0;2,0.25;"..S("This market limits the total number of items a given seller can have for sale at a time.\nYou have @1 items remaining. Cancel old sell orders to free up space.", sell_limit-total_sell).."]"
end
-- Buy, sell, quantity and price button
formspec[#formspec+1] = "tooltip[0,0.25;3.75,1;"..S("Use these fields to enter buy and sell orders for the selected item.").."]"
.."button[0,0.55;1,1;buy;"..S("Buy").."]field[1.2,0.85;1,1;quantity;"..S("Quantity")..";]"
.."field[2.1,0.85;1,1;price;"..S("Price per")..";]button[2.7,0.55;1,1;sell;Sell]"
.."field_close_on_enter[quantity;false]field_close_on_enter[price;false]"
.."container_end[]"
-- table of buy and sell orders
.."tablecolumns[color;text;"
.."text,align=right,tooltip="..S("The price per item in this order.")..";"
.."text,align=right,tooltip="..S("The total amount of items in this particular order.")..";"
.."text,align=right,tooltip="..S("The total amount of items available at this price accounting for the other orders also currently being offered.")..";"
.."text,tooltip="..S("The name of the player who placed this order.\nDouble-click your own orders to cancel them.")..";"
.."text,align=right,tooltip="..S("How many days ago this order was placed.").."]"
.."table[0,6.5;9.75,3.5;orders;#FFFFFF,"..S("Order")..","..S("Price")..","..S("Quantity")..","..S("Total Volume")..","..S("Player")..","..S("Days Old")
local sell_volume = selected_row.sell_volume
for i, sell in ipairs(selected_row.sell_orders) do
formspec[#formspec+1] = ",#FF0000,"..S("Sell")..","
..sell.price..","
..sell.quantity..","
..sell_volume..","
..get_account_name(sell.account, account, anonymous)..","
..math.floor((current_time-sell.timestamp)/86400)
sell_volume = sell_volume - sell.quantity
end
local buy_volume = 0
local buy_orders = selected_row.buy_orders
local buy_count = #buy_orders
-- Show buy orders in reverse order
for i = buy_count, 1, -1 do
local buy = buy_orders[i]
buy_volume = buy_volume + buy.quantity
formspec[#formspec+1] = ",#00FF00,"..S("Buy")..","
..buy.price..","
..buy.quantity..","
..buy_volume..","
..get_account_name(buy.account, account, anonymous)..","
..math.floor((current_time-buy.timestamp)/86400)
end
formspec[#formspec+1] = "]"
else
formspec[#formspec+1] = "label[0.1,5.1;"..S("Select an item to view or place orders.").."]"
end
return table.concat(formspec)
end
-------------------------------------------------------------------------------------
-- Information formspec
--{item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
local log_to_string = function(market, log_entry, account)
local anonymous = market.def.anonymous
local purchaser = log_entry.purchaser
local seller = log_entry.seller
local purchaser_name
if purchaser == seller then
purchaser_name = S("yourself")
elseif anonymous and purchaser ~= account then
purchaser_name = S("someone")
elseif purchaser == account then
purchaser_name = S("you")
else
purchaser_name = purchaser.name
end
local seller_name
if anonymous and seller ~= account then
seller_name = S("someone")
elseif seller == account then
seller_name = S("you")
else
seller_name = seller.name
end
local colour
local new
local last_acknowledged = account.last_acknowledged or 0
if log_entry.timestamp > last_acknowledged then
colour = "#FFFF00"
new = true
else
colour = "#FFFFFF"
new = false
end
local show_itemnames = account.show_itemnames == "true"
local itemname = log_entry.item
if not show_itemnames then
local item_def = minetest.registered_items[log_entry.item]
if item_def then
itemname = minetest.formspec_escape(item_def.description:gsub("\n", " "))
end
end
local currency_symbol = market.def.currency_symbol
return colour .. S("On day @1 @2 sold @3 @4 to @5 at @6@7 each for a total of @8@9.",
math.ceil(log_entry.timestamp/86400), seller_name, log_entry.quantity, itemname,
purchaser_name, currency_symbol, log_entry.price, currency_symbol, log_entry.quantity*log_entry.price), new
end
local get_info_formspec = function(market, account)
local formspec = {
"size[10,10]"
.."tabheader[0,0;tabs;"..market.def.description..","..S("Your Inventory")..","..S("Market Orders")..";1;false;true]"
.."textarea[0.75,0.5;9.25,1.5;;"..S("Description:")..";"..market.def.long_description.."]"
.."label[0.5,2.2;"..S("Your Recent Purchases and Sales:").."]"
.."textlist[0.5,2.6;8.75,4;log_entries;"
}
if next(account.log) then
local new = false
for _, log_entry in ipairs(account.log) do
local log_string, new_log = log_to_string(market, log_entry, account)
new = new or new_log
formspec[#formspec+1] = log_string
formspec[#formspec+1] = ","
end
formspec[#formspec] = "]" -- Note: there's no +1 here deliberately, that way the "]" overwrites the last comma added by the loop above.
if new then
formspec[#formspec+1] = "button[7.1,6.9;2,0.5;acknowledge_log;"..S("Mark logs as read").."]" ..
"tooltip[acknowledge_log;"..S("Log entries in yellow are new since last time you marked your log as read.").."]"
end
else
formspec[#formspec+1] = "#CCCCCC"..S("No logged activities in this market yet.").."]"
end
local show_itemnames = account.show_itemnames or "false"
formspec[#formspec+1] = "]container[0.5, 7.5]label[0,0;Settings:]checkbox[0,0.25;show_itemnames;"..S("Show Itemnames")..";"
..show_itemnames.."]"
if global_enable_item_icons then
local show_icons = account.show_icons or "true"
formspec[#formspec+1] = "checkbox[2,0.25;show_icons;"..S("Show Icons")..";"..show_icons.."]"
end
formspec[#formspec+1] = "container_end[]"
return table.concat(formspec)
end
---------------------------------------------------------------------------------------
commoditymarket.get_formspec = function(market, account)
local tab = account.tab
if tab == 1 then
return get_info_formspec(market, account)
elseif tab == 2 then
return get_account_formspec(market, account)
else
return get_market_formspec(market, account)
end
end
------------------------------------------------------------------------------------
-- Handling recieve_fields
local add_to_player_inventory = function(name, item, amount)
local playerinv = minetest.get_inventory({type="player", name=name})
local not_full = true
while amount > 0 and not_full do
local stack = ItemStack(item .. " " .. amount)
amount = amount - stack:get_count()
local leftover = playerinv:add_item("main", stack)
if leftover:get_count() > 0 then
amount = amount + leftover:get_count()
return amount
end
end
return amount
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
local formname_split = formname:split(":")
if formname_split[1] ~= "commoditymarket" then
return false
end
local market = commoditymarket.registered_markets[formname_split[2]]
if not market then
return false
end
local name = formname_split[3]
if name ~= player:get_player_name() then
return false
end
local account = market:get_account(name)
local show_icons = global_enable_item_icons and ((account.show_icons or "true") == "true")
local something_changed = false
if fields.tabs then
account.tab = tonumber(fields.tabs)
something_changed = true
end
-- player clicked on an item in the market summary table
if fields.summary then
local summaryevent = minetest.explode_table_event(fields.summary)
if summaryevent.type == "DCL" or summaryevent.type == "CHG" then
if summaryevent.row == 1 then
-- header clicked, sort by column
local column = tonumber(summaryevent.column)
if not (column == 1 and show_icons) then -- ignore clicks on the icon column header
account.sort_markets_by_column = column
end
else
-- item clicked, recreate the list to find out which one
local marketlist = make_marketlist(market, account)
local selected = marketlist[summaryevent.row-1]
if selected then
account.selected = selected.item
end
end
elseif summaryevent.type == "INV" then
account.selected = nil
end
something_changed = true
end
if fields.orders then
local ordersevent = minetest.explode_table_event(fields.orders)
if ordersevent.type == "DCL" and ordersevent.column > 0 then
local selected_idx = ordersevent.row - 1 -- account for header
local selected_row = market.orders_for_items[account.selected] -- sell orders come first
local sell_orders = selected_row.sell_orders
local sell_order_count = #sell_orders
local selected_order
if selected_idx <= sell_order_count then -- if the index is within the range of sell orders,
selected_order = sell_orders[selected_idx]
if selected_order and selected_order.account == account then -- and the order belongs to the current player,
market:cancel_sell(account.selected, selected_order) -- cancel it
something_changed = true
end
else
-- otherwise we're in the buy group, shift the index up by sell_order_count and reverse index order
local buy_orders = selected_row.buy_orders
local buy_orders_count = #buy_orders
selected_order = buy_orders[buy_orders_count - (selected_idx - sell_order_count - 1)]
if selected_order and selected_order.account == account then
market:cancel_buy(account.selected, selected_order)
something_changed = true
end
end
end
end
if fields.buy then
local quantity = tonumber(fields.quantity)
local price = tonumber(fields.price)
if price ~= nil and quantity ~= nil then
market:buy(name, account.selected, quantity, price)
something_changed = true
end
end
if fields.sell then
local quantity = tonumber(fields.quantity)
local price = tonumber(fields.price)
if price ~= nil and quantity ~= nil then
market:sell(name, account.selected, quantity, price)
something_changed = true
end
end
-- player clicked in their inventory table, may need to give him his stuff back
if fields.inventory then
local invevent = minetest.explode_table_event(fields.inventory)
if invevent.type == "DCL" and invevent.column > 0 then
local col_count = 8
local show_itemnames = account.show_itemnames == "true"
if not show_itemnames then
col_count = col_count - 2
end
if not show_icons then
col_count = col_count - 2
end
local index = math.floor(((invevent.row-1)*col_count + invevent.column - 1)/(col_count/2)) - 1
local account = market:get_account(name)
-- build a local copy of the inventory that would be displayed in the formspec so we can
-- figure out what item the index we were given is pointing to
local inventory = {}
for item, quantity in pairs(account.inventory) do
table.insert(inventory, {item=item, quantity=quantity, description=get_item_description(item)})
end
if show_itemnames then
table.sort(inventory, inventory_item_comp)
else
table.sort(inventory, inventory_desc_comp)
end
if inventory[index] then
local item = inventory[index].item
local amount = account.inventory[item]
local remaining = add_to_player_inventory(name, item, amount)
if remaining == 0 then
account.inventory[item] = nil
else
account.inventory[item] = remaining
end
if remaining ~= amount then
something_changed = true
end
end
end
end
if fields.withdraw or fields.key_enter_field == "withdrawamount" then
local withdrawvalue = tonumber(fields.withdrawamount)
if withdrawvalue then
local account = market:get_account(name)
withdrawvalue = math.min(withdrawvalue, account.balance)
for _, currency in ipairs(market.def.currency_ordered) do
this_unit_amount = math.floor(withdrawvalue/currency.amount)
if this_unit_amount > 0 then
local remaining = add_to_player_inventory(name, currency.item, this_unit_amount)
local value_given = (this_unit_amount - remaining) * currency.amount
account.balance = account.balance - value_given
withdrawvalue = withdrawvalue - value_given
something_changed = true
end
end
end
end
if fields.search_filter then
local value = string.lower(fields.search_filter)
if account.search ~= value then
account.search = value
end
end
local process_checkbox = function(property_name, fields, account)
if (fields[property_name] == "true" and account[property_name] ~= "true") or
(fields[property_name] == "false" and account[property_name] ~= "false") then
account[property_name] = fields[property_name]
return true
end
return false
end
if process_checkbox("filter_participating", fields, account) then something_changed = true end
if process_checkbox("show_itemnames", fields, account) then something_changed = true end
if process_checkbox("show_icons", fields, account) then something_changed = true end
if fields.acknowledge_log then
account.last_acknowledged = minetest.get_gametime()
something_changed = true
end
if fields.apply_search or fields.key_enter_field == "search_filter" then
something_changed = true
end
if fields.clear_search then
account.search = ""
something_changed = true
end
if something_changed then
minetest.show_formspec(name, formname, market:get_formspec(account))
end
end)

View File

@ -1,418 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
# Running params
params = {"recursive": False,
"help": False,
"mods": False,
"verbose": False,
"folders": []
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"mods": ['--installed-mods'],
"verbose": ['--verbose', '-v']
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 60
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["mods"])}
run on locally installed modules
{', '.join(options["verbose"])}
add output information
''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
elif params["recursive"] and params["mods"]:
print("Option --installed-mods is incompatible with --recursive")
else:
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if params["mods"]:
print(f"on all locally installed modules in {os.path.abspath('~/.minetest/mods/')}")
run_all_subfolders("~/.minetest/mods")
elif len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(f)
else:
update_folder(f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(params["folders"][0])
else:
update_folder(params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(os.path.abspath("./"))
else:
update_folder(os.path.abspath("./"))
#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
pattern_tr = re.compile(r'(.+?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')
#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
pass
return None
#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out
# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
# The first three items are for unused matches
text = re.sub(r'#~ msgid "', "", text)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# comment lines
text = re.sub(r'#.*\n', "", text)
# converting msg pairs into "=" pairs
text = re.sub(r'msgid "', "", text)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove double-spaced lines
text = re.sub(r'\n\n', '\n', text)
return text
# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = modname + "." + language_code + ".tr"
tr_file = os.path.join(root, tr_name)
if os.path.exists(tr_file):
if params["verbose"]:
print(f"{tr_name} already exists, ignoring {name}")
continue
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
if params["verbose"]:
print(f"Importing translations from {name}")
text = process_po_file(po_file.read())
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name):
lOut = [f"# textdomain: {mod_name}\n"]
dGroupedBySource = {}
for key in dkeyStrings:
sourceList = list(dkeyStrings[key])
sourceList.sort()
sourceString = "\n".join(sourceList)
listForSource = dGroupedBySource.get(sourceString, [])
listForSource.append(key)
dGroupedBySource[sourceString] = listForSource
lSourceKeys = list(dGroupedBySource.keys())
lSourceKeys.sort()
for source in lSourceKeys:
localizedStrings = dGroupedBySource[source]
localizedStrings.sort()
lOut.append("")
lOut.append(source)
lOut.append("")
for localizedString in localizedStrings:
val = dOld.get(localizedString, {})
translation = val.get("translation", "")
comment = val.get("comment")
if len(localizedString) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{localizedString}={translation}")
if len(localizedString) > doublespace_threshold:
lOut.append("")
unusedExist = False
for key in dOld:
if key not in dkeyStrings:
val = dOld[key]
translation = val.get("translation")
comment = val.get("comment")
# only keep an unused translation if there was translated
# text or a comment associated with it
if translation != None and (translation != "" or comment):
if not unusedExist:
unusedExist = True
lOut.append("\n\n##### not used anymore #####\n")
if len(key) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{key}={translation}")
if len(key) > doublespace_threshold:
lOut.append("")
return "\n".join(lOut) + '\n'
# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
# read existing template file to preserve comments
existing_template = import_tr_file(templ_file)
text = strings_to_text(dkeyStrings, existing_template[0], mod_name)
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write(text)
# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
#TODO remove comments here
text = re.sub(pattern_concat, "", text)
strings = []
for s in pattern_lua.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed.findall(text):
strings.append(s)
for s in strings:
s = re.sub(r'"\.\.\s+"', "", s)
s = re.sub("@[^@=0-9]", "@@", s)
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
def import_tr_file(tr_file):
dOut = {}
text = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
if line[:3] == "###":
# Reset comment block if we hit a header
latest_comment_block = None
continue
if line[:1] == "#":
# Save the comment we're inside
if not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
dOut[match.group(1)] = outval
return (dOut, text)
# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
dOut = {}
for root, dirs, files in os.walk(folder):
for name in files:
if fnmatch.fnmatch(name, "*.lua"):
fname = os.path.join(root, name)
found = read_lua_file_strings(fname)
if params["verbose"]:
print(f"{fname}: {str(len(found))} translatable strings")
for s in found:
sources = dOut.get(s, set())
sources.add(f"### {os.path.basename(fname)} ###")
dOut[s] = sources
if len(dOut) == 0:
return None
templ_file = os.path.join(folder, "locale/template.txt")
write_template(templ_file, dOut, mod_name)
return dOut
# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
if params["verbose"]:
print(f"updating {tr_file}")
tr_import = import_tr_file(tr_file)
dOld = tr_import[0]
textOld = tr_import[1]
textNew = strings_to_text(dNew, dOld, mod_name)
if textOld and textOld != textNew:
print(f"{tr_file} has changed.")
shutil.copyfile(tr_file, f"{tr_file}.old")
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write(textNew)
# Updates translation files for the mod in the given folder
def update_mod(folder):
modname = get_modname(folder)
if modname is not None:
process_po_files(folder, modname)
print(f"Updating translations for {modname}")
data = generate_template(folder, modname)
if data == None:
print(f"No translatable strings found in {modname}")
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
else:
print("Unable to find modname in folder " + folder)
# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
for subfolder in subfolders:
update_mod(subfolder + "/")
else:
update_mod(folder)
print("Done.")
def run_all_subfolders(folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir()]:
update_folder(modfolder + "/")
main()

View File

@ -1,6 +0,0 @@
commoditymarket = {}
local MP = minetest.get_modpath(minetest.get_current_modname())
dofile(MP.."/formspecs.lua")
dofile(MP.."/market.lua")
dofile(MP.."/doc.lua")

View File

@ -1,176 +0,0 @@
# textdomain: commoditymarket
### doc.lua ###
#long-form description of how markets work, for documentation
At the core of how a market operates are "buy" and "sell" orders. A buy order is an announcement to the world that you are interested in purchasing a certain quantity of item and are willing to pay a certain amount of currency in exchange for each unit of that item. Conversely, a sell order is an announcement to the world that you are interested in selling a certain quantity of item and will accept a certain amount of currency in exchange for each unit of that item.@n@nThe market price of an item is determined by where the existing buy and sell orders for that item intersect. When you offer to buy an item for a price that someone is offering to sell it at, the item is transferred to you and currency is transferred from your account to theirs to cover the cost. The market will keep track of the most recent price that an item was successfully sold for, but note that this information is for historical interest only - there's no guarantee that anyone is currently willing to match the historical price.@n@nWhen an item is selected in the upper list, the currently existing buy and sell orders for that item will be displayed in the lower list. Sell orders are listed first in descending price, followed by buy orders in ascending price. The current market price will be somewhere in between the lowest sell order and the highest buy order. If you wish to cancel a buy or sell order that you've placed for an item, double-click on the order and the item or currency that you put into that order will be returned to your inventory.@n@nIf you place a buy order and there are already sell orders for the item that meet or are below your price, some or all of your buy order might be immediately fulfilled. Your purchases will be made at the price that the sell orders have been set to - if you were willing to pay 15 units of currency per item but someone was already offering to sell for 2 units of currency per item, you only pay 2 units for each of that offer's items. If there aren't enough compatible sell orders to fulfill your buy order, the remainder will be placed into the market and made available for future sellers to see and fulfill if they agree to your price. Your buy order will immediately deduct the currency required for it from your account's balance, but if you cancel your order you will get that currency back - it's not gone until the order is actually fulfilled.@n@nIf you place a sell order and there are already buy orders that meet or exceed your price, some or all of your sell order may be immediately fulfilled. You'll be paid the price that the buyers are offering rather than the amount you're demanding. If any of your sell offer is left unfulfilled, the sell order will be added to the market for future buyers to see. The items for this offer will be immediately taken from your market inventory but if you cancel your order you will get those items back.=
#documentation tab text
Commodity Markets=
#long-form description of how markets work, for documentation
Each player's account has an inventory that serves as a holding area for items that are destined to be sold or that have been bought by the player but not yet retrieved. This inventory is a bit different from the standard Minetest inventory in that it doesn't hold item "stacks", it just tracks the total number of that item present. Some markets allow for extremely large quantities of an item to be stored here for sale.@n@nTo add an item to your market inventory for eventual sale either shift-click on the item in your player inventory or drag the item stack to the inventory slot below the main market inventory list. Some markets may have restrictions on what items can be bought and sold, if an item is not valid for that market it won't go into the market's inventory. Some items are considered "currency" and will add to your account's currency balance instead of being listed in your market inventory.@n@nTools cannot be added to the market inventory if they have any wear on them. The market also can't handle items with attached metadata such as books that have had text added to them.@n@nTo remove an item from your market inventory, double-click in it in the market inventory list. As much of the item as can fit into your player inventory will be transferred to you, with any remainder staying behind in the market inventory. To withdraw currency from your market balance type the amount you'd like to withdraw in the field next to the "Withdraw" button. The currency will be converted into items and added to your player inventory, with whatever cannot be converted remaining behind in your market balance.=
#documentation category description
Game-wide marketplaces where goods can be bought and sold at prices of your choice.=
#title of documentation page
User Interface: Inventory=
#title of documentation page
User Interface: Orders=
### formspecs.lua ###
#tooltip
All the items you've transfered to the market to sell and the items you've@npurchased with buy orders. Double-click on an item to bring it back into your@npersonal inventory.=
#tooltip
Apply search to outputs.=
Balance:=
#label
#button label
Buy=
#table column header
Buy Max=
#table column header
Buy Vol=
#tooltip
Clear search.=
Days Old=
#table column header
Description=
Description:=
#label for inventory drop target. The hard return is needed to fit it into the UI
Drop items here to@nadd to your account=
#tooltip
Enter substring to search item identifiers for.=
#tooltip
Enter the amount of currency you'd like to withdraw then click the 'Withdraw'@nbutton to convert it into items and transfer it to your personal inventory.=
How many days ago this order was placed.=
#label
In inventory:=
#table column header
Inventory=
#label
Inventory limit:=
#table column header
Item=
#table column header
Last Price=
#tooltip
Log entries in yellow are new since last time you marked your log as read.=
Mark logs as read=
#tab label
Market Orders=
#tooltip
Maximum price being offered to buy one of these.=
#tooltip
Minimum price being demanded to sell one of these.=
#checkbox label
My orders=
#shown if there are no transactions the player has participated in to list in the log
No logged activities in this market yet.=
#tooltip
Number of items available for sale in the market.=
#tooltip
Number of items there's demand for in the market.=
#transaction log entry. @1 is a day number, @2 is a player name, @3 is a quantity of items, @4 is the item name, @5 is a player name, @6 is a currency symbol, @7 is a number (price), @8 is the currency symbol again and @9 is a number (price)
On day @1 @2 sold @3 @4 to @5 at @6@7 each for a total of @8@9.=
Order=
Player=
Price=
#tooltip
Price paid for one of these the last time one was sold.=
Price per=
#table column header, field label
Quantity=
#tooltip
Quantity of this item that you have in your inventory ready to sell.=
Select an item to view or place orders.=
#tooltip
Select this to show only the markets where you have either a buy or a sell order pending.=
Sell=
#table column header
Sell Min=
#table column header
Sell Vol=
Sell limit:=
#checkbox label
Show Icons=
#checkbox label
Show Itemnames=
The name of the player who placed this order.@nDouble-click your own orders to cancel them.=
The price per item in this order.=
The total amount of items available at this price accounting for the other orders also currently being offered.=
The total amount of items in this particular order.=
#tooltip
This market limits the total number of items a given seller can have for sale at a time.@nYou have @1 items remaining. Cancel old sell orders to free up space.=
Total Volume=
#undefined item
Unknown Item=
#tooltip
Use these fields to enter buy and sell orders for the selected item.=
#button label
Withdraw=
#tooltip
You can still receive purchased items if you've exceeded your inventory limit,@nbut you won't be able to transfer items from your personal inventory into@nthe market until you've emptied it back down below the limit again.=
#tab label
Your Inventory=
Your Recent Purchases and Sales:=
someone=
you=
yourself=
### market.lua ###
1 @1 @= @2@3=
A market where orders to buy or sell items can be placed and fulfilled.=
Add all registered items to the provided market=
Currency item values:=
Market=
Market has unlimited inventory space.=
Market inventory is limited to @1 items.=
Market supports unlimited pending sell orders.=
Purging item: @1 from market: @2=
Total pending sell orders are limited to @1 items.=
You can't afford that many of this item.=
You can't pay less than nothing for an item.=
You can't sell fewer than one item.=
You can't sell items for a negative price.=
You don't have enough of that item in your inventory to post this sell order.=
You have to buy at least one item.=
You have too many items listed for sale in this market, please cancel some sell orders to make room for new ones.=
list all registered markets=
remove item from market. All existing buys and sells will be canceled.=
removes all unknown items from all markets. All existing buys and sells for those items will be canceled.=
show market interface=

View File

@ -1,718 +0,0 @@
local S = minetest.get_translator(minetest.get_current_modname())
commoditymarket.registered_markets = {}
local log_length_limit = 30
-- from http://lua-users.org/wiki/BinaryInsert
--[[
table.bininsert( table, value [, comp] )
Inserts a given value through BinaryInsert into the table sorted by [, comp].
If 'comp' is given, then it must be a function that receives
two table elements, and returns true when the first is less
than the second, e.g. comp = function(a, b) return a > b end,
will give a sorted table, with the biggest value on position 1.
[, comp] behaves as in table.sort(table, value [, comp])
returns the index where 'value' was inserted
]]--
local comp_default = function(a, b) return a < b end
function table.bininsert(t, value, comp)
-- Initialise compare function
local comp = comp or comp_default
-- Initialise numbers
local iStart, iEnd, iMid, iState = 1, #t, 1, 0
-- Get insert position
while iStart <= iEnd do
-- calculate middle
iMid = math.floor( (iStart+iEnd)/2 )
-- compare
if comp(value, t[iMid]) then
iEnd, iState = iMid - 1, 0
else
iStart, iState = iMid + 1, 1
end
end
local target = iMid+iState
table.insert(t, target, value)
return target
end
-- lowest price first
local buy_comp = function(order1, order2)
local price1 = order1.price
local price2 = order2.price
if price1 < price2 then
return true
elseif price1 == price2 and order1.timestamp < order2.timestamp then
return true
end
return false
end
-- highest price first
local sell_comp = function(order1, order2)
local price1 = order1.price
local price2 = order2.price
if price1 > price2 then
return true
elseif price1 == price2 and order1.timestamp < order2.timestamp then
return true
end
return false
end
---------------------------------
local get_account = function(market, player_name)
local account = market.player_accounts[player_name]
if account then
return account
end
account = {}
account.search = ""
account.name = player_name
account.balance = 0 -- currency
account.inventory = {} -- items stored in the market inventory that aren't part of sell orders yet. stored as "[item] = count"
account.filter_participating = "false"
account.log = {} -- might want to use a more sophisticated queue, but this isn't going to be a big list so that's more trouble than it's worth right now.
market.player_accounts[player_name] = account
return account
end
-- Caution: the data structures produced by sale logging caused me to discover
-- issue https://github.com/minetest/minetest/issues/8719 with minetest.serialize()
-- I'm working around it by using the code in persistence.lua instead
local log_sale = function(item, quantity, price, purchaser, seller)
local log_entry = {item=item, quantity=quantity, price=price, purchaser=purchaser, seller=seller, timestamp = minetest.get_gametime()}
local purchaser_log = purchaser.log
local seller_log = seller.log
table.insert(purchaser_log, log_entry)
if #purchaser_log > log_length_limit then
table.remove(purchaser_log, 1)
end
if (purchaser ~= seller) then
table.insert(seller_log, log_entry)
if #seller_log > log_length_limit then
table.remove(seller_log, 1)
end
end
end
local remove_orders_by_account = function(orders, account)
if not orders then return end
local i = 1
while i < #orders do
local order = orders[i]
if order.account == account then
table.remove(orders, i)
else
i = i + 1
end
end
end
local remove_account = function(player_name)
local account = player_accounts[player_name]
if account == nil then
return
end
player_accounts[player_name] = nil
for item, lists in pairs(market) do
remove_orders_by_account(lists.buy_orders, account)
remove_orders_by_account(lists.sell_orders, account)
end
end
------------------------------------------------------------------------------------------
local add_inventory_to_account = function(market, account, item, quantity)
if quantity < 1 then
return false
end
if market.def.currency[item] then
account.balance = account.balance + market.def.currency[item] * quantity
else
account.inventory[item] = (account.inventory[item] or 0) + quantity
end
return true
end
local remove_inventory_from_account = function(account, item, quantity)
if quantity < 1 then
return false
end
local inventory = account.inventory
local current_quantity = inventory[item] or 0
if current_quantity < quantity then
return false
end
local new_quantity = current_quantity - quantity
if new_quantity == 0 then
inventory[item] = nil
else
inventory[item] = new_quantity
end
return true
end
local remove_order = function(order, array)
for i, market_order in ipairs(array) do
if order == market_order then
table.remove(array, i)
return true
end
end
return false
end
-----------------------------------------------------------------------------------------------------------
local add_sell = function(market, account, item, price, quantity)
price = tonumber(price)
quantity = tonumber(quantity)
local sell_limit = market.def.sell_limit
local sell_limit_exceeded
if sell_limit then
local total_sell = 0
for item, orders in pairs(market.orders_for_items) do
for _, order in ipairs(orders.sell_orders) do
if order.account == account then
total_sell = total_sell + order.quantity
end
end
end
sell_limit_exceeded = total_sell + quantity > sell_limit
end
-- validate that this sell order is possible
if sell_limit_exceeded or price < 0 or quantity < 1 or not remove_inventory_from_account(account, item, quantity) then
minetest.sound_play({name = "commoditymarket_error", gain = 0.1}, {to_player=account.name})
if sell_limit_exceeded then
minetest.chat_send_player(account.name, S("You have too many items listed for sale in this market, please cancel some sell orders to make room for new ones."))
elseif price < 0 then
minetest.chat_send_player(account.name, S("You can't sell items for a negative price."))
elseif quantity < 1 then
minetest.chat_send_player(account.name, S("You can't sell fewer than one item."))
else
minetest.chat_send_player(account.name, S("You don't have enough of that item in your inventory to post this sell order."))
end
return false
end
local buy_market = market.orders_for_items[item].buy_orders
local buy_order = buy_market[#buy_market]
local current_buy_volume = market.orders_for_items[item].buy_volume
-- go through existing buy orders that are more expensive than or equal to the price
-- we're demanding, selling them at the order's price until we run out of
-- buy orders or run out of demand
while quantity > 0 and buy_order and buy_order.price >= price do
local quantity_to_sell = math.min(buy_order.quantity, quantity)
quantity = quantity - quantity_to_sell
local earned = quantity_to_sell*buy_order.price
account.balance = account.balance + earned
add_inventory_to_account(market, buy_order.account, item, quantity_to_sell)
buy_order.quantity = buy_order.quantity - quantity_to_sell
current_buy_volume = current_buy_volume - quantity_to_sell
if buy_order.account ~= account then
-- don't update the last price if a player is just buying and selling from themselves
market.orders_for_items[item].last_price = buy_order.price
end
log_sale(item, quantity_to_sell, buy_order.price, buy_order.account, account)
if buy_order.quantity == 0 then
table.remove(buy_market, #buy_market)
end
buy_order = buy_market[#buy_market]
end
market.orders_for_items[item].buy_volume = current_buy_volume
if quantity > 0 then
local sell_market = market.orders_for_items[item].sell_orders
-- create the order and insert it into order arrays
local order = {account=account, price=price, quantity=quantity, timestamp=minetest.get_gametime()}
table.bininsert(sell_market, order, sell_comp)
market.orders_for_items[item].sell_volume = market.orders_for_items[item].sell_volume + quantity
end
minetest.sound_play({name = "commoditymarket_register_opened", gain = 0.1}, {to_player=account.name})
return true
end
local cancel_sell = function(market, item, order)
local account = order.account
local quantity = order.quantity
local sell_market = market.orders_for_items[item].sell_orders
remove_order(order, sell_market)
market.orders_for_items[item].sell_volume = market.orders_for_items[item].sell_volume - quantity
add_inventory_to_account(market, account, item, quantity)
minetest.sound_play({name = "commoditymarket_register_closed", gain = 0.1}, {to_player=account.name})
end
-----------------------------------------------------------------------------------------------------------
local test_buy = function(market, balance, item, price, quantity)
local sell_market = market.orders_for_items[item].sell_orders
local test_quantity = quantity
local test_balance = balance
local i = 0
local sell_order = sell_market[#sell_market]
while test_quantity > 0 and sell_order and sell_order.price <= price do
local quantity_to_buy = math.min(sell_order.quantity, test_quantity)
test_quantity = test_quantity - quantity_to_buy
test_balance = test_balance - quantity_to_buy*sell_order.price
i = i + 1
sell_order = sell_market[#sell_market-i]
end
local spent = balance - test_balance
test_balance = test_balance - test_quantity*price
if test_balance < 0 then
return false, spent, test_quantity
end
return true, spent, test_quantity
end
local add_buy = function(market, account, item, price, quantity)
price = tonumber(price)
quantity = tonumber(quantity)
if price < 0 or quantity < 1 or not test_buy(market, account.balance, item, price, quantity) then
minetest.sound_play({name = "commoditymarket_error", gain = 0.1}, {to_player=account.name})
if price < 0 then
minetest.chat_send_player(account.name, S("You can't pay less than nothing for an item."))
elseif quantity < 1 then
minetest.chat_send_player(account.name, S("You have to buy at least one item."))
else
minetest.chat_send_player(account.name, S("You can't afford that many of this item."))
end
return false
end
local sell_market = market.orders_for_items[item].sell_orders
local sell_order = sell_market[#sell_market]
local current_sell_volume = market.orders_for_items[item].sell_volume
-- go through existing sell orders that are cheaper than or equal to the price
-- we're wanting to offer, buying them up at the offered price until we run out of
-- sell orders or run out of supply
while quantity > 0 and sell_order and sell_order.price <= price do
local quantity_to_buy = math.min(sell_order.quantity, quantity)
quantity = quantity - quantity_to_buy
local spent = quantity_to_buy*sell_order.price
account.balance = account.balance - spent
sell_order.account.balance = sell_order.account.balance + spent
sell_order.quantity = sell_order.quantity - quantity_to_buy
current_sell_volume = current_sell_volume - quantity_to_buy
add_inventory_to_account(market, account, item, quantity_to_buy)
if sell_order.account ~= account then
-- don't update the last price if a player is just buying and selling from themselves
market.orders_for_items[item].last_price = sell_order.price
end
log_sale(item, quantity_to_buy, sell_order.price, account, sell_order.account)
-- Sell order completely used up, remove it
if sell_order.quantity == 0 then
table.remove(sell_market, #sell_market)
end
-- get the next sell order
sell_order = sell_market[#sell_market]
end
market.orders_for_items[item].sell_volume = current_sell_volume
if quantity > 0 then
local buy_market = market.orders_for_items[item].buy_orders
-- create the order for the remainder and insert it into order arrays
local order = {account=account, price=price, quantity=quantity, timestamp=minetest.get_gametime()}
account.balance = account.balance - quantity*price -- buy orders are pre-paid
table.bininsert(buy_market, order, buy_comp)
market.orders_for_items[item].buy_volume = market.orders_for_items[item].buy_volume + quantity
end
minetest.sound_play({name = "commoditymarket_register_opened", gain = 0.1}, {to_player=account.name})
return true
end
local cancel_buy = function(market, item, order)
local account = order.account
local quantity = order.quantity
local price = order.price
local buy_market = market.orders_for_items[item].buy_orders
market.orders_for_items[item].buy_volume = market.orders_for_items[item].buy_volume - quantity
remove_order(order, buy_market)
account.balance = account.balance + price*quantity
minetest.sound_play({name = "commoditymarket_register_closed", gain = 0.1}, {to_player=account.name})
end
local initialize_market_item = function(orders_for_items, item)
if orders_for_items[item] == nil then
local lists = {}
lists.buy_orders = {}
lists.sell_orders = {}
lists.buy_volume = 0
lists.sell_volume = 0
lists.item = item
-- leave last_price nil to indicate it's never been sold before
orders_for_items[item] = lists
end
end
-----------------------------------------------------------------------------------------------------------
-- Chat commands
minetest.register_chatcommand("market.show", {
params = "marketname",
privs = {server=true},
description = S("show market interface"),
func = function(name, param)
local market = commoditymarket.registered_markets[param]
if market == nil then return end
local formspec = market:get_formspec(market:get_account(name))
minetest.show_formspec(name, "commoditymarket:"..param..":"..name, formspec)
end,
})
minetest.register_chatcommand("market.list", {
params = "",
privs = {server=true},
description = S("list all registered markets"),
func = function(name, param)
local list = {}
for marketname, def in pairs(commoditymarket.registered_markets) do
table.insert(list, marketname)
end
table.sort(list)
minetest.chat_send_player(name, "Registered markets: " .. table.concat(list, ", "))
end,
})
local remove_market_item = function(market, item)
local marketitem = market.orders_for_items[item]
if marketitem then
local buy_orders = marketitem.buy_orders
while #buy_orders > 0 do
market:cancel_buy(item, buy_orders[#buy_orders])
end
local sell_orders = marketitem.sell_orders
while #sell_orders > 0 do
market:cancel_sell(item, sell_orders[#sell_orders])
end
market.orders_for_items[item] = nil
end
end
minetest.register_chatcommand("market.removeitem", {
params = "marketname item",
privs = {server=true},
description = S("remove item from market. All existing buys and sells will be canceled."),
func = function(name, param)
local params = param:split(" ")
if #params ~= 2 then
minetest.chat_send_player(name, "Incorrect parameter count")
return
end
local market = commoditymarket.registered_markets[params[1]]
if market == nil then
minetest.chat_send_player(name, "No such market: " .. params[1])
return
end
remove_market_item(market, params[2])
end,
})
minetest.register_chatcommand("market.purge_unknowns", {
params = "",
privs = {server=true},
description = S("removes all unknown items from all markets. All existing buys and sells for those items will be canceled."),
func = function(name, param)
for market_name, market in pairs(commoditymarket.registered_markets) do
local items_to_remove = {}
local items_to_move = {}
for item, orders in pairs(market.orders_for_items) do
local icon = commoditymarket.get_icon(item)
if icon == "unknown_item.png" then
table.insert(items_to_remove, item)
end
end
for _, item in ipairs(items_to_remove) do
minetest.chat_send_player(name, S("Purging item: @1 from market: @2", tostring(item), market_name))
minetest.log("warning", "[commoditymarket] Purging unknown item: " .. tostring(item) .. " from market: " .. market_name)
remove_market_item(market, item)
end
end
end,
})
-- Used during development and debugging to find items that break the market formspecs when added
local debugging_commands = false
if debugging_commands then
minetest.register_chatcommand("market.addeverything", {
params = "marketname",
privs = {server=true},
description = S("Add all registered items to the provided market"),
func = function(name, param)
local params = param:split(" ")
if #params ~= 1 then
minetest.chat_send_player(name, "Incorrect parameter count")
return
end
local market = commoditymarket.registered_markets[params[1]]
if market == nil then
minetest.chat_send_player(name, "No such market: " .. params[1])
return
end
for item_name, def in pairs(minetest.registered_items) do
initialize_market_item(market.orders_for_items, item_name)
end
end,
})
end
-----------------------------------------------------------------------------------------------------------
-- API exposed to the outside world
local add_inventory = function(self, player_name, item, quantity)
return add_inventory_to_account(self, get_account(self, player_name), item, quantity)
end
local remove_inventory = function(self, player_name, item, quantity)
return remove_inventory_from_account(get_account(self, player_name), item, quantity)
end
local sell = function(self, player_name, item, quantity, price)
return add_sell(self, get_account(self, player_name), item, price, quantity)
end
local buy = function(self, player_name, item, quantity, price)
return add_buy(self, get_account(self, player_name), item, price, quantity)
end
-- Using this instead of minetest.serialize because of https://github.com/minetest/minetest/issues/8719
local MP = minetest.get_modpath(minetest.get_current_modname())
local persistence_store, persistence_load = dofile(MP.."/persistence.lua")
local worldpath = minetest.get_worldpath()
local load_market_data = function(marketname)
local filename = worldpath .. "/market_"..marketname..".lua"
return persistence_load(filename)
end
local save_market_data = function(market)
local filename = worldpath .. "/market_"..market.name..".lua"
local data = {}
data.player_accounts = market.player_accounts
data.orders_for_items = market.orders_for_items
persistence_store(filename, data)
return true
end
local make_doc_entry = function() return end
if minetest.get_modpath("doc") then
make_doc_entry = function(market_name, market_def)
local currencies = {}
for _, currency_item in ipairs(market_def.currency_ordered) do
local item_def = minetest.registered_items[currency_item.item]
table.insert(currencies, S("1 @1 = @2@3", item_def.description, market_def.currency_symbol, currency_item.amount))
end
local inventory_limit
if market_def.inventory_limit then
inventory_limit = S("Market inventory is limited to @1 items.", market_def.inventory_limit)
else
inventory_limit = S("Market has unlimited inventory space.")
end
local sell_limit
if market_def.sell_limit then
sell_limit = S("Total pending sell orders are limited to @1 items.", market_def.inventory_limit)
else
sell_limit = S("Market supports unlimited pending sell orders.")
end
doc.add_entry("commoditymarket", "market_"..market_name, {
name = market_def.description,
data = { text = market_def.long_description
.."\n\n"
..S("Currency item values:") .. "\n " .. table.concat(currencies, "\n ")
.."\n\n"
..inventory_limit
.."\n"
..sell_limit
}
})
end
end
commoditymarket.register_market = function(market_name, market_def)
assert(not commoditymarket.registered_markets[market_name])
assert(market_def.currency)
market_def.currency_symbol = market_def.currency_symbol or "¤" -- \u{00A4} -- defaults to the generic currency symbol ("scarab")
market_def.description = market_def.description or S("Market")
market_def.long_description = market_def.long_description or S("A market where orders to buy or sell items can be placed and fulfilled.")
-- Reprocess currency table into a form easier for the withdraw code to work with
market_def.currency_ordered = {}
for item, amount in pairs(market_def.currency) do
table.insert(market_def.currency_ordered, {item=item, amount=amount})
end
table.sort(market_def.currency_ordered, function(currency1, currency2) return currency1.amount > currency2.amount end)
make_doc_entry(market_name, market_def) -- market_def has now been normalized, make documentation for it if doc is installed.
-- Just in case a developer supplied strings that don't work well in formspecs, escape them now so we don't have to do it
-- wherever they're used.
market_def.currency_symbol = minetest.formspec_escape(market_def.currency_symbol)
market_def.description = minetest.formspec_escape(market_def.description)
market_def.long_description = minetest.formspec_escape(market_def.long_description)
local new_market = {}
new_market.def = market_def
commoditymarket.registered_markets[market_name] = new_market
local loaded_data = load_market_data(market_name)
if loaded_data then
new_market.player_accounts = loaded_data.player_accounts
new_market.orders_for_items = loaded_data.orders_for_items
else
new_market.player_accounts = {}
new_market.orders_for_items = {}
end
-- If there's a list of initial items in the market def, initialize them. allow_item can trump this.
local initial_items = market_def.initial_items
if initial_items then
-- defer until after to ensure that all initial items have been registered, so we can guard against invalid items
minetest.after(0,
function()
for _, item in ipairs(initial_items) do
if minetest.registered_items[item] and
((not market_def.allow_item) or market_def.allow_item(item)) and
not market_def.currency[item] then
initialize_market_item(new_market.orders_for_items, item)
end
end
end)
end
market_def.initial_items = nil -- don't need this any more
new_market.name = market_name
new_market.add_inventory = add_inventory
new_market.remove_inventory = remove_inventory
new_market.sell = sell
new_market.buy = buy
new_market.cancel_sell = cancel_sell
new_market.cancel_buy = cancel_buy
new_market.get_formspec = commoditymarket.get_formspec
new_market.get_account = get_account
new_market.save = save_market_data
-- save markets on shutdown
minetest.register_on_shutdown(function() new_market:save() end)
-- and also every ten minutes, to be on the safe side in case Minetest crashes
-- TODO: a more sophisticated approach that checks whether the market data is "dirty" before actually saving
local until_next_save = 600
minetest.register_globalstep(function(dtime)
until_next_save = until_next_save - dtime
if until_next_save < 0 then
new_market:save()
until_next_save = 600
end
end)
----------------------------------------------------------------------
-- Detached inventory for adding items into the market
local inv = minetest.create_detached_inventory("commoditymarket:"..market_name, {
allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
return 0
end,
allow_put = function(inv, listname, index, stack, player)
local item = stack:get_name()
-- reject unknown items
if minetest.registered_items[item] == nil then
return 0
end
-- Currency items are always allowed
if new_market.def.currency[item] then
return stack:get_count()
end
-- only new tools, no used tools
if stack:get_wear() ~= 0 then
return 0
end
--nothing with metadata permitted
local meta = stack:get_meta():to_table()
local fields = meta.fields
local inventory = meta.inventory
if (fields and next(fields)) or (inventory and next(inventory)) then
return 0
end
-- If there's no allow_item function defined, allow everything. Otherwise check if the item is allowed
if (not market_def.allow_item) or market_def.allow_item(item) then
local allowed_count = stack:get_count()
if market_def.inventory_limit then
-- limit additions to the inventory_limit, if there is one
local current_count = 0
for _, inventory_quantity in pairs(new_market:get_account(player:get_player_name()).inventory) do
current_count = current_count + inventory_quantity
end
allowed_count = math.min(allowed_count, allowed_count + market_def.inventory_limit - (current_count+allowed_count))
if allowed_count <= 0 then return 0 end
end
--ensures the item is in the market listing if it wasn't before
initialize_market_item(new_market.orders_for_items, item)
return allowed_count
end
return 0
end,
allow_take = function(inv, listname, index, stack, player)
return 0
end,
on_move = function(inv, from_list, from_index, to_list, to_index, count, player)
end,
on_take = function(inv, listname, index, stack, player)
end,
on_put = function(inv, listname, index, stack, player)
if listname == "add" then
local item = stack:get_name()
local count = stack:get_count()
new_market:add_inventory(player:get_player_name(), item, count)
inv:set_list("add", {})
local name = player:get_player_name()
local formspec = new_market:get_formspec(new_market:get_account(name))
minetest.show_formspec(name, "commoditymarket:"..market_name..":"..name, formspec)
end
end
})
inv:set_size("add", 1)
end
commoditymarket.show_market = function(market_name, player_name)
local market = commoditymarket.registered_markets[market_name]
if market == nil then return end
local formspec = market:get_formspec(market:get_account(player_name))
minetest.show_formspec(player_name, "commoditymarket:"..market_name..":"..player_name, formspec)
end

View File

@ -1,3 +0,0 @@
name = commoditymarket
description = Provides API support for various in-world commodity markets
optional_depends = doc

View File

@ -1,200 +0,0 @@
-- Internal persistence library
--[[ Provides ]]
-- persistence.store(path, ...): Stores arbitrary items to the file at the given path
-- persistence.load(path): Loads files that were previously stored with store and returns them
--[[ Limitations ]]
-- Does not export userdata, threads or most function values
-- Function export is not portable
--[[ License: MIT (see bottom) ]]
-- Private methods
local write, writeIndent, writers, refCount;
-- write thing (dispatcher)
write = function (file, item, level, objRefNames)
writers[type(item)](file, item, level, objRefNames);
end;
-- write indent
writeIndent = function (file, level)
for i = 1, level do
file:write("\t");
end;
end;
-- recursively count references
refCount = function (objRefCount, item)
-- only count reference types (tables)
if type(item) == "table" then
-- Increase ref count
if objRefCount[item] then
objRefCount[item] = objRefCount[item] + 1;
else
objRefCount[item] = 1;
-- If first encounter, traverse
for k, v in pairs(item) do
refCount(objRefCount, k);
refCount(objRefCount, v);
end;
end;
end;
end;
-- Format items for the purpose of restoring
writers = {
["nil"] = function (file, item)
file:write("nil");
end;
["number"] = function (file, item)
file:write(tostring(item));
end;
["string"] = function (file, item)
file:write(string.format("%q", item));
end;
["boolean"] = function (file, item)
if item then
file:write("true");
else
file:write("false");
end
end;
["table"] = function (file, item, level, objRefNames)
local refIdx = objRefNames[item];
if refIdx then
-- Table with multiple references
file:write("multiRefObjects["..refIdx.."]");
else
-- Single use table
file:write("{\n");
for k, v in pairs(item) do
writeIndent(file, level+1);
file:write("[");
write(file, k, level+1, objRefNames);
file:write("] = ");
write(file, v, level+1, objRefNames);
file:write(";\n");
end
writeIndent(file, level);
file:write("}");
end;
end;
["function"] = function (file, item)
-- Does only work for "normal" functions, not those
-- with upvalues or c functions
local dInfo = debug.getinfo(item, "uS");
if dInfo.nups > 0 then
file:write("nil --[[functions with upvalue not supported]]");
elseif dInfo.what ~= "Lua" then
file:write("nil --[[non-lua function not supported]]");
else
local r, s = pcall(string.dump,item);
if r then
file:write(string.format("loadstring(%q)", s));
else
file:write("nil --[[function could not be dumped]]");
end
end
end;
["thread"] = function (file, item)
file:write("nil --[[thread]]\n");
end;
["userdata"] = function (file, item)
file:write("nil --[[userdata]]\n");
end;
}
return function (path, ...)
local file, e;
if type(path) == "string" then
-- Path, open a file
file, e = io.open(path, "w");
if not file then
return error(e);
end
else
-- Just treat it as file
file = path;
end
local n = select("#", ...);
-- Count references
local objRefCount = {}; -- Stores reference that will be exported
for i = 1, n do
refCount(objRefCount, (select(i,...)));
end;
-- Export Objects with more than one ref and assign name
-- First, create empty tables for each
local objRefNames = {};
local objRefIdx = 0;
file:write("-- Persistent Data\n");
file:write("local multiRefObjects = {\n");
for obj, count in pairs(objRefCount) do
if count > 1 then
objRefIdx = objRefIdx + 1;
objRefNames[obj] = objRefIdx;
file:write("{};"); -- table objRefIdx
end;
end;
file:write("\n} -- multiRefObjects\n");
-- Then fill them (this requires all empty multiRefObjects to exist)
for obj, idx in pairs(objRefNames) do
for k, v in pairs(obj) do
file:write("multiRefObjects["..idx.."][");
write(file, k, 0, objRefNames);
file:write("] = ");
write(file, v, 0, objRefNames);
file:write(";\n");
end;
end;
-- Create the remaining objects
for i = 1, n do
file:write("local ".."obj"..i.." = ");
write(file, (select(i,...)), 0, objRefNames);
file:write("\n");
end
-- Return them
if n > 0 then
file:write("return obj1");
for i = 2, n do
file:write(" ,obj"..i);
end;
file:write("\n");
else
file:write("return\n");
end;
file:close();
end, function (path)
local f, e = loadfile(path);
if f then
return f();
else
return nil, e;
end;
end
--[[
Copyright (c) 2010 Gerhard Roethlin
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
]]

View File

@ -1,95 +0,0 @@
This mod implements marketplaces where players can post buy and sell offers for various items, allowing for organic market forces to determine the relative values of the resources in a world.
The basic market interface is the same across all markets and market types, but this mod allows for a variety of different ways that markets can be configured to support different playstyles. Markets can have restrictions on what they will allow to be bought and sold, different types of "currency", and can share a common inventory across multiple locations or can be localized to just one spot at the discretion of the server owner.
![](screenshot.png)
## Currency
Each market has one or more "currency" items defined that are treated differently from the other items that can be bought and sold there. Currency items are translated into a player's currency balance rather than being bought and sold directly.
For example, some of the markets offered by the "commoditymarket_fantasy" mod have this currency definition:
{
["default:gold_ingot"] = 1000,
["commoditymarket_fantasy:gold_coin"] = 1
}
When a gold ingot is added to the player's market account it turns into 1000 units of currency. When a gold coin is added it turns into 1 unit of currency. You can't buy and sell gold directly in this market, it is instead the "standard" by which the value of other items is measured.
There's no reason that all markets in a given world have to use the same currency. Having variety in currency types adds flavour to the world and also introduces opportunities for enterprising traders to make a profit by moneychanging between different marketplaces.
## Account Inventory
In addition to tracking a player's currency balance, each player's account has an inventory that serves as a holding area for items that are destined to be sold or that have been bought by the player but not yet retrieved. This inventory is a bit different from the standard Minetest inventory in that it doesn't hold individual item "stacks", allowing for larger quantities of items to be accumulated than would otherwise be practical. If a player needs to buy 20,000 stone bricks for a major construction project then their account's inventory will hold that.
To prevent abuse of the market inventory as a free storage space, or just to add some unique flavor to a particular market, a limit on the inventory's size can be added. This limit only affects transfers from a player's personal inventory into the market inventory; the limit can be exceeded by incoming items being sold to the player.
Note that tools cannot be added to the market inventory if they have any wear on them, nor can the market handle items with attached metadata (such as books that have had text added to them).
## Placing a "Buy" Order
A buy order is an offer to give a certain amount of currency in exchange for a particular type of item. To place a buy order go to the "Market Orders" tab of the market's interface and select the item from the list of items on the market. If the item isn't listed it may be that the market is simply "unaware" of the item's existence; try placing an example of the item into your personal inventory and if the item is permitted on the market a new entry will be added to Market Orders.
Enter the quantity and price you desire and then click the "buy" button to place a buy order.
If there are already "sell" orders for the item when you place a buy order, some or all of your buy order might be immediately fulfilled provided you are offering a sufficient price. Your purchases will be made at the price that the sell orders have been set to - if you were willing to pay 15 units of currency per item but someone was already offering to sell for 2 units of currency per item, you only pay 2 units for each of that offer's items.
If there aren't enough compatible sell orders to fulfill your buy order, the remainder will be placed into the market and made available for future sellers to see and fulfill if they agree to your price. Your buy order will immediately deduct the currency required for it from your account's balance, but if you cancel your order you will get that currency back - it's not gone until the order is actually fulfilled.
Double-click on your order in the orders list to cancel it.
## Placing a "Sell" Order
Sell orders are an offer of a certain amount of an item and a price you're willing to accept in exchange for them. They're placed in a similar manner to buy orders, except by clicking the "sell" button instead of the "buy" button.
If there are already buyers with buy orders that meet or exceed your price, some or all of your sell order may be immediately fulfilled. You'll be paid the price that the buyers are offering rather than the amount you're demanding.
If any of your sell offer is left unfulfilled, the sell order will be added to the market for future buyers to see. The items for this offer will be immediately taken from your market inventory but if you cancel your order you will get those items back.
Double-click on your order in the orders list to cancel it.
## Commands
This mod has several commands that a server administrator can use:
* `market.removeitem marketname item` -- cancels all existing buy and sell orders for an item and removes its entry from the market tab. This is useful if you've changed what items are permitted in a particular market and need to clear out items that are no longer allowed.
* `market.show marketname` -- opens the market's formspec
* `market.list` -- lists the marketnames of all registered markets
* `market.purge_unknowns` -- executes "removeitem" for all markets on all items that don't have a definition. Useful for clearing out items that are no longer defined due to a mod being updated or removed.
* `market.addeverything marketname` - Adds all registered items to a market's listings. NOTE: this is intended as a debugging tool, not for use in a live server, as it doesn't filter out items that a player cannot actually harvest in-world.
## Registering a market
The mod "[commoditymarket_fantasy](https://github.com/FaceDeer/commoditymarket_fantasy)" contains a number of pre-defined markets that provide examples of what's possible with this mod. They include:
* King's Market - a basic sort of "commoner's marketplace", only open during the day
* Night Market - the shadier side of commerce, only open during the night
* Trader's Caravan - a type of market that players can build and place themselves, with a small inventory capacity.
* Goblin Exchange - a strange marketplace that uses coal as a currency
* Undermarket - where dark powers make their trades, using Mese as a currency
All of these except for the Trader's Caravan are intended to be placed in specific locations by server administrators or mapgen, they don't have crafting recipes. Modifying these markets or creating your own from scratch should hopefully be a fairly straightforward task.
### Market definition API
```
local market_def = {
description = "Night Market", -- A short name for this market, appears as the text of the "info" tab of the market's UI
long_description = "When the sun sets and the stalls of the King's Market close, other vendors are just waking up to share their wares. The Night Market is not as voluminous as the King's Market but accepts a wider range of wares. It accepts the same gold coinage of the realm, one thousand coins to the gold ingot.", -- A longer description with flavor text and other information to present to the user, shown in the info tab. Optional.
currency = {
["default:gold_ingot"] = 1000,
["commoditymarket:gold_coins"] = 1
}, -- List all items that get translated into "currency" here, along with their conversion rates. Take care to ensure there's no way for a player to multiply their money when crafting currency items into each other (eg, if there was some way to get more than 1000 coin items out of a gold ingot, in this case)
currency_symbol = "☼", -- Used in various places in the UI. If not defined, defaults to "¤" (the generic currency symbol)
inventory_limit = 10000, -- Optional, when set this prevents the player from adding items to their market inventory when it's over this limit
sell_limit = 10000, -- Optional, when set this prevents sell orders from being added if the player already has this many items for sale
initial_items = {"default:cobble", "default:wood"}, -- Optional, a list of items that the market will be initialized with on startup. Players can add other items during play.
allow_item = function(item) return true end, -- Optional, this function is used to determine whether the market permits a player to add a particular item to its inventory.
anonymous = true, -- If set to true then the player won't be able to see the names associated with other player's orders, only their own.
}
commoditymarket.register_market("market_name", market_def)
```
Once a market is defined, use `commoditymarket.show_market(market_name, player_name)` to show the market interface to a player.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,8 +0,0 @@
#Some item images are very large and break the market formspec.
#See https://github.com/minetest/minetest/issues/9300
#Alternately, use:
#commoditymarket.override_item_icon(item_name, new_icon_texture)
#or
#commoditymarket.override_image_icon(old_icon_texture, new_icon_texture)
#to override a troublesome image directly.
commoditymarket_enable_item_icons (Enable item icon images in market formspecs) bool true

View File

@ -1,3 +0,0 @@
commoditymarket_register_closed.ogg - from https://freesound.org/people/bspiller5/sounds/180252/ by bspiller5 under the CC-BY 3.0 license
commoditymarket_register_opened.ogg - from https://freesound.org/people/kiddpark/sounds/201159/ by kiddpark under the CC-BY 3.0 license
commoditymarket_error.ogg - from https://freesound.org/people/LorenzoTheGreat/sounds/417794/ by LorenzoTheGreat under the CC-BY-SA 3.0 license

Binary file not shown.

Before

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
commoditymarket_search.png, commoditymarket_clear.png - Copyright © Diego Martínez (kaeza): CC BY-SA 3.0

View File

@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto

View File

@ -1,40 +0,0 @@
# Compiled Lua sources
luac.out
# luarocks build files
*.src.rock
*.zip
*.tar.gz
# Object files
*.o
*.os
*.ko
*.elf
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 FaceDeer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,418 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Script to generate the template file and update the translation files.
# Copy the script into the mod or modpack root folder and run it there.
#
# Copyright (C) 2019 Joachim Stolberg, 2020 FaceDeer, 2020 Louis Royer
# LGPLv2.1+
from __future__ import print_function
import os, fnmatch, re, shutil, errno
from sys import argv as _argv
# Running params
params = {"recursive": False,
"help": False,
"mods": False,
"verbose": False,
"folders": []
}
# Available CLI options
options = {"recursive": ['--recursive', '-r'],
"help": ['--help', '-h'],
"mods": ['--installed-mods'],
"verbose": ['--verbose', '-v']
}
# Strings longer than this will have extra space added between
# them in the translation files to make it easier to distinguish their
# beginnings and endings at a glance
doublespace_threshold = 60
def set_params_folders(tab: list):
'''Initialize params["folders"] from CLI arguments.'''
# Discarding argument 0 (tool name)
for param in tab[1:]:
stop_param = False
for option in options:
if param in options[option]:
stop_param = True
break
if not stop_param:
params["folders"].append(os.path.abspath(param))
def set_params(tab: list):
'''Initialize params from CLI arguments.'''
for option in options:
for option_name in options[option]:
if option_name in tab:
params[option] = True
break
def print_help(name):
'''Prints some help message.'''
print(f'''SYNOPSIS
{name} [OPTIONS] [PATHS...]
DESCRIPTION
{', '.join(options["help"])}
prints this help message
{', '.join(options["recursive"])}
run on all subfolders of paths given
{', '.join(options["mods"])}
run on locally installed modules
{', '.join(options["verbose"])}
add output information
''')
def main():
'''Main function'''
set_params(_argv)
set_params_folders(_argv)
if params["help"]:
print_help(_argv[0])
elif params["recursive"] and params["mods"]:
print("Option --installed-mods is incompatible with --recursive")
else:
# Add recursivity message
print("Running ", end='')
if params["recursive"]:
print("recursively ", end='')
# Running
if params["mods"]:
print(f"on all locally installed modules in {os.path.abspath('~/.minetest/mods/')}")
run_all_subfolders("~/.minetest/mods")
elif len(params["folders"]) >= 2:
print("on folder list:", params["folders"])
for f in params["folders"]:
if params["recursive"]:
run_all_subfolders(f)
else:
update_folder(f)
elif len(params["folders"]) == 1:
print("on folder", params["folders"][0])
if params["recursive"]:
run_all_subfolders(params["folders"][0])
else:
update_folder(params["folders"][0])
else:
print("on folder", os.path.abspath("./"))
if params["recursive"]:
run_all_subfolders(os.path.abspath("./"))
else:
update_folder(os.path.abspath("./"))
#group 2 will be the string, groups 1 and 3 will be the delimiters (" or ')
#See https://stackoverflow.com/questions/46967465/regex-match-text-in-either-single-or-double-quote
pattern_lua = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*(["\'])((?:\\\1|(?:(?!\1)).)*)(\1)[\s,\)]', re.DOTALL)
pattern_lua_bracketed = re.compile(r'[\.=^\t,{\(\s]N?S\(\s*\[\[(.*?)\]\][\s,\)]', re.DOTALL)
# Handles "concatenation" .. " of strings"
pattern_concat = re.compile(r'["\'][\s]*\.\.[\s]*["\']', re.DOTALL)
pattern_tr = re.compile(r'(.+?[^@])=(.*)')
pattern_name = re.compile(r'^name[ ]*=[ ]*([^ \n]*)')
pattern_tr_filename = re.compile(r'\.tr$')
pattern_po_language_code = re.compile(r'(.*)\.po$')
#attempt to read the mod's name from the mod.conf file. Returns None on failure
def get_modname(folder):
try:
with open(os.path.join(folder, "mod.conf"), "r", encoding='utf-8') as mod_conf:
for line in mod_conf:
match = pattern_name.match(line)
if match:
return match.group(1)
except FileNotFoundError:
pass
return None
#If there are already .tr files in /locale, returns a list of their names
def get_existing_tr_files(folder):
out = []
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
if pattern_tr_filename.search(name):
out.append(name)
return out
# A series of search and replaces that massage a .po file's contents into
# a .tr file's equivalent
def process_po_file(text):
# The first three items are for unused matches
text = re.sub(r'#~ msgid "', "", text)
text = re.sub(r'"\n#~ msgstr ""\n"', "=", text)
text = re.sub(r'"\n#~ msgstr "', "=", text)
# comment lines
text = re.sub(r'#.*\n', "", text)
# converting msg pairs into "=" pairs
text = re.sub(r'msgid "', "", text)
text = re.sub(r'"\nmsgstr ""\n"', "=", text)
text = re.sub(r'"\nmsgstr "', "=", text)
# various line breaks and escape codes
text = re.sub(r'"\n"', "", text)
text = re.sub(r'"\n', "\n", text)
text = re.sub(r'\\"', '"', text)
text = re.sub(r'\\n', '@n', text)
# remove header text
text = re.sub(r'=Project-Id-Version:.*\n', "", text)
# remove double-spaced lines
text = re.sub(r'\n\n', '\n', text)
return text
# Go through existing .po files and, if a .tr file for that language
# *doesn't* exist, convert it and create it.
# The .tr file that results will subsequently be reprocessed so
# any "no longer used" strings will be preserved.
# Note that "fuzzy" tags will be lost in this process.
def process_po_files(folder, modname):
for root, dirs, files in os.walk(os.path.join(folder, 'locale/')):
for name in files:
code_match = pattern_po_language_code.match(name)
if code_match == None:
continue
language_code = code_match.group(1)
tr_name = modname + "." + language_code + ".tr"
tr_file = os.path.join(root, tr_name)
if os.path.exists(tr_file):
if params["verbose"]:
print(f"{tr_name} already exists, ignoring {name}")
continue
fname = os.path.join(root, name)
with open(fname, "r", encoding='utf-8') as po_file:
if params["verbose"]:
print(f"Importing translations from {name}")
text = process_po_file(po_file.read())
with open(tr_file, "wt", encoding='utf-8') as tr_out:
tr_out.write(text)
# from https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python/600612#600612
# Creates a directory if it doesn't exist, silently does
# nothing if it already exists
def mkdir_p(path):
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else: raise
# Converts the template dictionary to a text to be written as a file
# dKeyStrings is a dictionary of localized string to source file sets
# dOld is a dictionary of existing translations and comments from
# the previous version of this text
def strings_to_text(dkeyStrings, dOld, mod_name):
lOut = [f"# textdomain: {mod_name}\n"]
dGroupedBySource = {}
for key in dkeyStrings:
sourceList = list(dkeyStrings[key])
sourceList.sort()
sourceString = "\n".join(sourceList)
listForSource = dGroupedBySource.get(sourceString, [])
listForSource.append(key)
dGroupedBySource[sourceString] = listForSource
lSourceKeys = list(dGroupedBySource.keys())
lSourceKeys.sort()
for source in lSourceKeys:
localizedStrings = dGroupedBySource[source]
localizedStrings.sort()
lOut.append("")
lOut.append(source)
lOut.append("")
for localizedString in localizedStrings:
val = dOld.get(localizedString, {})
translation = val.get("translation", "")
comment = val.get("comment")
if len(localizedString) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{localizedString}={translation}")
if len(localizedString) > doublespace_threshold:
lOut.append("")
unusedExist = False
for key in dOld:
if key not in dkeyStrings:
val = dOld[key]
translation = val.get("translation")
comment = val.get("comment")
# only keep an unused translation if there was translated
# text or a comment associated with it
if translation != None and (translation != "" or comment):
if not unusedExist:
unusedExist = True
lOut.append("\n\n##### not used anymore #####\n")
if len(key) > doublespace_threshold and not lOut[-1] == "":
lOut.append("")
if comment != None:
lOut.append(comment)
lOut.append(f"{key}={translation}")
if len(key) > doublespace_threshold:
lOut.append("")
return "\n".join(lOut) + '\n'
# Writes a template.txt file
# dkeyStrings is the dictionary returned by generate_template
def write_template(templ_file, dkeyStrings, mod_name):
# read existing template file to preserve comments
existing_template = import_tr_file(templ_file)
text = strings_to_text(dkeyStrings, existing_template[0], mod_name)
mkdir_p(os.path.dirname(templ_file))
with open(templ_file, "wt", encoding='utf-8') as template_file:
template_file.write(text)
# Gets all translatable strings from a lua file
def read_lua_file_strings(lua_file):
lOut = []
with open(lua_file, encoding='utf-8') as text_file:
text = text_file.read()
#TODO remove comments here
text = re.sub(pattern_concat, "", text)
strings = []
for s in pattern_lua.findall(text):
strings.append(s[1])
for s in pattern_lua_bracketed.findall(text):
strings.append(s)
for s in strings:
s = re.sub(r'"\.\.\s+"', "", s)
s = re.sub("@[^@=0-9]", "@@", s)
s = s.replace('\\"', '"')
s = s.replace("\\'", "'")
s = s.replace("\n", "@n")
s = s.replace("\\n", "@n")
s = s.replace("=", "@=")
lOut.append(s)
return lOut
# Gets strings from an existing translation file
# returns both a dictionary of translations
# and the full original source text so that the new text
# can be compared to it for changes.
def import_tr_file(tr_file):
dOut = {}
text = None
if os.path.exists(tr_file):
with open(tr_file, "r", encoding='utf-8') as existing_file :
# save the full text to allow for comparison
# of the old version with the new output
text = existing_file.read()
existing_file.seek(0)
# a running record of the current comment block
# we're inside, to allow preceeding multi-line comments
# to be retained for a translation line
latest_comment_block = None
for line in existing_file.readlines():
line = line.rstrip('\n')
if line[:3] == "###":
# Reset comment block if we hit a header
latest_comment_block = None
continue
if line[:1] == "#":
# Save the comment we're inside
if not latest_comment_block:
latest_comment_block = line
else:
latest_comment_block = latest_comment_block + "\n" + line
continue
match = pattern_tr.match(line)
if match:
# this line is a translated line
outval = {}
outval["translation"] = match.group(2)
if latest_comment_block:
# if there was a comment, record that.
outval["comment"] = latest_comment_block
latest_comment_block = None
dOut[match.group(1)] = outval
return (dOut, text)
# Walks all lua files in the mod folder, collects translatable strings,
# and writes it to a template.txt file
# Returns a dictionary of localized strings to source file sets
# that can be used with the strings_to_text function.
def generate_template(folder, mod_name):
dOut = {}
for root, dirs, files in os.walk(folder):
for name in files:
if fnmatch.fnmatch(name, "*.lua"):
fname = os.path.join(root, name)
found = read_lua_file_strings(fname)
if params["verbose"]:
print(f"{fname}: {str(len(found))} translatable strings")
for s in found:
sources = dOut.get(s, set())
sources.add(f"### {os.path.basename(fname)} ###")
dOut[s] = sources
if len(dOut) == 0:
return None
templ_file = os.path.join(folder, "locale/template.txt")
write_template(templ_file, dOut, mod_name)
return dOut
# Updates an existing .tr file, copying the old one to a ".old" file
# if any changes have happened
# dNew is the data used to generate the template, it has all the
# currently-existing localized strings
def update_tr_file(dNew, mod_name, tr_file):
if params["verbose"]:
print(f"updating {tr_file}")
tr_import = import_tr_file(tr_file)
dOld = tr_import[0]
textOld = tr_import[1]
textNew = strings_to_text(dNew, dOld, mod_name)
if textOld and textOld != textNew:
print(f"{tr_file} has changed.")
shutil.copyfile(tr_file, f"{tr_file}.old")
with open(tr_file, "w", encoding='utf-8') as new_tr_file:
new_tr_file.write(textNew)
# Updates translation files for the mod in the given folder
def update_mod(folder):
modname = get_modname(folder)
if modname is not None:
process_po_files(folder, modname)
print(f"Updating translations for {modname}")
data = generate_template(folder, modname)
if data == None:
print(f"No translatable strings found in {modname}")
else:
for tr_file in get_existing_tr_files(folder):
update_tr_file(data, modname, os.path.join(folder, "locale/", tr_file))
else:
print("Unable to find modname in folder " + folder)
# Determines if the folder being pointed to is a mod or a mod pack
# and then runs update_mod accordingly
def update_folder(folder):
is_modpack = os.path.exists(os.path.join(folder, "modpack.txt")) or os.path.exists(os.path.join(folder, "modpack.conf"))
if is_modpack:
subfolders = [f.path for f in os.scandir(folder) if f.is_dir()]
for subfolder in subfolders:
update_mod(subfolder + "/")
else:
update_mod(folder)
print("Done.")
def run_all_subfolders(folder):
for modfolder in [f.path for f in os.scandir(folder) if f.is_dir()]:
update_folder(modfolder + "/")
main()

View File

@ -1,544 +0,0 @@
local modpath = minetest.get_modpath(minetest.get_current_modname())
minetest.register_alias("commoditymarket:kings_market", "commoditymarket_fantasy:kings_market")
minetest.register_alias("commoditymarket:gold_coins", "commoditymarket_fantasy:gold_coins")
minetest.register_alias("commoditymarket:night_market", "commoditymarket_fantasy:night_market")
minetest.register_alias("commoditymarket:goblin_market", "commoditymarket_fantasy:goblin_market")
minetest.register_alias("commoditymarket:under_market", "commoditymarket_fantasy:under_market")
minetest.register_alias("commoditymarket:caravan_post", "commoditymarket_fantasy:caravan_post")
minetest.register_alias("commoditymarket:caravan_market_1", "commoditymarket_fantasy:caravan_market_1")
minetest.register_alias("commoditymarket:caravan_market_2", "commoditymarket_fantasy:caravan_market_2")
minetest.register_alias("commoditymarket:caravan_market_3", "commoditymarket_fantasy:caravan_market_3")
minetest.register_alias("commoditymarket:caravan_market_4", "commoditymarket_fantasy:caravan_market_4")
minetest.register_alias("commoditymarket:caravan_market_5", "commoditymarket_fantasy:caravan_market_5")
minetest.register_alias("commoditymarket:caravan_market_permanent", "commoditymarket_fantasy:caravan_market_permanent")
local S = minetest.get_translator(minetest.get_current_modname())
dofile(modpath.."/mapgen_dungeon_markets.lua")
local coins_per_ingot = math.floor(tonumber(minetest.settings:get("commoditymarket_coins_per_ingot")) or 1000)
-- Only register gold coins once, if required
local gold_coins_registered = false
local register_gold_coins = function()
if not gold_coins_registered then
minetest.register_craftitem("commoditymarket_fantasy:gold_coins", {
description = S("Gold Coins"),
_doc_items_longdesc = S("A gold ingot is far too valuable to use as a basic unit of value, so it has become common practice to divide the standard gold bar into @1 small disks to make trade easier.", coins_per_ingot),
_doc_items_usagehelp = S("Gold coins can be deposited and withdrawn from markets that accept them as currency. These markets can make change if you have @1 coins and would like them back in ingot form again.", coins_per_ingot),
inventory_image = "commoditymarket_gold_coins.png",
stack_max = coins_per_ingot,
})
gold_coins_registered = true
end
end
local default_items = {"default:axe_bronze","default:axe_diamond","default:axe_mese","default:axe_steel","default:axe_steel","default:axe_stone","default:axe_wood","default:pick_bronze","default:pick_diamond","default:pick_mese","default:pick_steel","default:pick_stone","default:pick_wood","default:shovel_bronze","default:shovel_diamond","default:shovel_mese","default:shovel_steel","default:shovel_stone","default:shovel_wood","default:sword_bronze","default:sword_diamond","default:sword_mese","default:sword_steel","default:sword_stone","default:sword_wood", "default:blueberries", "default:book", "default:bronze_ingot", "default:clay_brick", "default:clay_lump", "default:coal_lump", "default:copper_ingot", "default:copper_lump", "default:diamond", "default:flint", "default:gold_ingot", "default:gold_lump", "default:iron_lump", "default:mese_crystal", "default:mese_crystal_fragment", "default:obsidian_shard", "default:paper", "default:steel_ingot", "default:stick", "default:tin_ingot", "default:tin_lump", "default:acacia_tree", "default:acacia_wood", "default:apple", "default:aspen_tree", "default:aspen_wood", "default:blueberry_bush_sapling", "default:bookshelf", "default:brick", "default:bronzeblock", "default:bush_sapling", "default:cactus", "default:clay", "default:coalblock", "default:cobble", "default:copperblock", "default:desert_cobble", "default:desert_sand", "default:desert_sandstone", "default:desert_sandstone_block", "default:desert_sandstone_brick", "default:desert_stone", "default:desert_stone_block", "default:desert_stonebrick", "default:diamondblock", "default:dirt", "default:glass", "default:goldblock", "default:gravel", "default:ice", "default:junglegrass", "default:junglesapling", "default:jungletree", "default:junglewood", "default:ladder_steel", "default:ladder_wood", "default:large_cactus_seedling", "default:mese", "default:mese_post_light", "default:meselamp", "default:mossycobble", "default:obsidian", "default:obsidian_block", "default:obsidian_glass", "default:obsidianbrick", "default:papyrus", "default:pine_sapling", "default:pine_tree", "default:pine_wood", "default:sand", "default:sandstone", "default:sandstone_block", "default:sandstonebrick", "default:sapling", "default:silver_sand", "default:silver_sandstone", "default:silver_sandstone_block", "default:silver_sandstone_brick", "default:snow", "default:snowblock", "default:steelblock", "default:stone", "default:stone_block", "default:stonebrick", "default:tinblock", "default:tree", "default:wood",}
local usage_help = S("Right-click on this to open the market interface.")
------------------------------------------------------------------------------
-- King's Market
if minetest.settings:get_bool("commoditymarket_enable_kings_market", true) then
local kings_def = {
description = S("King's Market"),
long_description = S("The largest and most accessible market for the common man, the King's Market uses gold coins as its medium of exchange (or the equivalent in gold ingots - @1 coins to the ingot). However, as a respectable institution of the surface world, the King's Market operates only during the hours of daylight. The purchase and sale of swords and explosives is prohibited in the King's Market. Gold coins are represented by a '☼' symbol.", coins_per_ingot),
currency = {
["default:gold_ingot"] = coins_per_ingot,
["commoditymarket_fantasy:gold_coins"] = 1
},
currency_symbol = "", -- "\u{263C}" Alchemical symbol for gold
allow_item = function(item)
if item:sub(1,13) == "default:sword" or item:sub(1,4) == "tnt:" then
return false
end
return true
end,
inventory_limit = 100000,
--sell_limit =, -- no sell limit for the King's Market
initial_items = default_items,
}
register_gold_coins()
commoditymarket.register_market("kings", kings_def)
local kings_protect = minetest.settings:get_bool("commoditymarket_protect_kings_market", true)
local on_blast
if kings_protect then
on_blast = function() end
end
minetest.register_node("commoditymarket_fantasy:kings_market", {
description = kings_def.description,
_doc_items_longdesc = kings_def.long_description,
_doc_items_usagehelp = usage_help,
tiles = {"default_chest_top.png","default_chest_top.png",
"default_chest_side.png","default_chest_side.png",
"commoditymarket_empty_shelf.png","default_chest_side.png^commoditymarket_crown.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
local timeofday = minetest.get_timeofday()
if timeofday > 0.2 and timeofday < 0.8 then
commoditymarket.show_market("kings", clicker:get_player_name())
else
minetest.chat_send_player(clicker:get_player_name(), S("At this time of day the King's Market is closed."))
minetest.sound_play({name = "commoditymarket_error", gain = 0.1}, {to_player=clicker:get_player_name()})
end
end,
can_dig = function(pos, player)
return not kings_protect or minetest.check_player_privs(player, "protection_bypass")
end,
on_blast = on_blast,
})
end
-------------------------------------------------------------------------------
-- Night Market
if minetest.settings:get_bool("commoditymarket_enable_night_market", true) then
local night_def = {
description = S("Night Market"),
long_description = S("When the sun sets and the stalls of the King's Market close, other vendors are just waking up to share their wares. The Night Market is not as voluminous as the King's Market but accepts a wider range of wares. It accepts the same gold coinage of the realm, @1 coins to the gold ingot.", coins_per_ingot),
currency = {
["default:gold_ingot"] = coins_per_ingot,
["commoditymarket_fantasy:gold_coins"] = 1
},
currency_symbol = "", --"\u{263C}"
inventory_limit = 10000,
--sell_limit =, -- no sell limit for the Night Market
initial_items = default_items,
anonymous = true,
}
register_gold_coins()
commoditymarket.register_market("night", night_def)
local night_protect = minetest.settings:get_bool("commoditymarket_protect_night_market", true)
local on_blast
if night_protect then
on_blast = function() end
end
minetest.register_node("commoditymarket_fantasy:night_market", {
description = night_def.description,
_doc_items_longdesc = night_def.long_description,
_doc_items_usagehelp = usage_help,
tiles = {"default_chest_top.png","default_chest_top.png",
"default_chest_side.png","default_chest_side.png",
"commoditymarket_empty_shelf.png","default_chest_side.png^commoditymarket_moon.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
local timeofday = minetest.get_timeofday()
if timeofday < 0.2 or timeofday > 0.8 then
commoditymarket.show_market("night", clicker:get_player_name())
else
minetest.chat_send_player(clicker:get_player_name(), S("At this time of day the Night Market is closed."))
minetest.sound_play({name = "commoditymarket_error", gain = 0.1}, {to_player=clicker:get_player_name()})
end
end,
can_dig = function(pos, player)
return not night_protect or minetest.check_player_privs(player, "protection_bypass")
end,
on_blast = on_blast,
})
end
-------------------------------------------------------------------------------
if minetest.settings:get_bool("commoditymarket_enable_caravan_market", true) then
-- "Trader's Caravan" - small-capacity market that players can summon
local time_until_caravan = 120 -- caravan arrives in two minutes
local dwell_time = 600 -- caravan leaves ten minutes after last usage
local caravan_def = {
description = S("Trader's Caravan"),
long_description = S("Unlike most markets that have well-known fixed locations that travelers congregate to, the network of Trader's Caravans is fluid and dynamic in their locations. A Trader's Caravan can show up anywhere, make modest trades, and then be gone the next time you visit them. These caravans accept gold and gold coins as a currency (one gold ingot to @1 gold coins exchange rate). Any reasonably-wealthy person can create a signpost marking a location where Trader's Caravans will make a stop.", coins_per_ingot),
currency = {
["default:gold_ingot"] = coins_per_ingot,
["commoditymarket_fantasy:gold_coins"] = 1
},
currency_symbol = "", --"\u{263C}"
inventory_limit = 1000,
sell_limit = 1000,
initial_items = default_items,
}
register_gold_coins()
minetest.register_craft({
output = "commoditymarket_fantasy:caravan_post",
recipe = {
{'group:wood', 'group:wood', ''},
{'group:wood', "default:gold_ingot", ''},
{'group:wood', "default:chest_locked", ''},
}
})
commoditymarket.register_market("caravan", caravan_def)
local create_caravan_def = function(override_table)
local def = {
description = caravan_def.description,
_doc_items_longdesc = caravan_def.long_description,
_doc_items_usagehelp = usage_help,
drawtype = "mesh",
mesh = "commoditymarket_wagon.obj",
tiles = {
{ name = "commoditymarket_door_wood.png", backface_culling = true }, -- door
{ name = "default_wood.png", backface_culling = true }, -- base wood
{ name = "default_fence_rail_wood.png", backface_culling = true }, -- wheel sides
{ name = "default_coal_block.png", backface_culling = true }, -- wheel tyre
{ name = "commoditymarket_shingles_wood.png", backface_culling = true }, -- roof
{ name = "default_junglewood.png", backface_culling = true }, -- corner wood
},
collision_box = {
type = "fixed",
fixed = {
-- Note: this oversized nodebox may cause slight problems when you stand on it.
-- https://github.com/minetest/minetest/issues/9322
{-0.75, -0.5, -1.25, 0.75, 1.5, 1.25},
},
},
selection_box = {
type = "fixed",
fixed = {
{-0.75, -0.5, -1.25, 0.75, 1.5, 1.25},
},
},
paramtype2 = "facedir",
drop = "",
groups = {choppy = 2, oddly_breakable_by_hand = 1, not_in_creative_inventory = 1},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("caravan", clicker:get_player_name())
local timer = minetest.get_node_timer(pos)
timer:start(dwell_time)
end,
after_destruct = function(pos, oldnode)
local facedir = oldnode.param2
local dir = minetest.facedir_to_dir(facedir)
local target = vector.add(pos, vector.multiply(dir,-3))
local target_node = minetest.get_node(target)
if target_node.name == "commoditymarket_fantasy:caravan_post" then
local meta = minetest.get_meta(target)
meta:set_string("infotext", S("Right-click to summon a trader's caravan"))
end
end,
on_timer = function(pos, elapsed)
minetest.set_node(pos, {name="air"})
minetest.sound_play("commoditymarket_register_closed", {
pos = pos,
gain = 1.0, -- default
max_hear_distance = 32, -- default, uses an euclidean metric
})
end,
}
if override_table then
for k, v in pairs(override_table) do
def[k] = v
end
end
return def
end
-- Create five caravans with different textures, randomly pick which one shows up.
minetest.register_node("commoditymarket_fantasy:caravan_market_1", create_caravan_def())
minetest.register_node("commoditymarket_fantasy:caravan_market_2", create_caravan_def({
tiles = {
{ name = "commoditymarket_door_wood.png^[multiply:#CCCCFF", backface_culling = true }, -- door
{ name = "default_acacia_wood.png", backface_culling = true }, -- base wood
{ name = "default_fence_rail_wood.png", backface_culling = true }, -- wheel sides
{ name = "default_copper_block.png", backface_culling = true }, -- wheel tyre
{ name = "commoditymarket_shingles_wood.png^[multiply:#CC8888", backface_culling = true }, -- roof
{ name = "default_wood.png", backface_culling = true }, -- corner wood
}
}))
minetest.register_node("commoditymarket_fantasy:caravan_market_3", create_caravan_def({
tiles = {
{ name = "commoditymarket_door_wood.png", backface_culling = true }, -- door
{ name = "default_aspen_wood.png", backface_culling = true }, -- base wood
{ name = "default_fence_aspen_wood.png", backface_culling = true }, -- wheel sides
{ name = "default_cobble.png", backface_culling = true }, -- wheel tyre
{ name = "default_stone_brick.png", backface_culling = true }, -- roof
{ name = "default_pine_tree.png", backface_culling = true }, -- corner wood
}
}))
minetest.register_node("commoditymarket_fantasy:caravan_market_4", create_caravan_def({
tiles = {
{ name = "commoditymarket_door_wood.png", backface_culling = true }, -- door
{ name = "default_junglewood.png", backface_culling = true }, -- base wood
{ name = "default_fence_rail_junglewood.png", backface_culling = true }, -- wheel sides
{ name = "default_obsidian.png", backface_culling = true }, -- wheel tyre
{ name = "commoditymarket_shingles_wood.png^[multiply:#88FF88", backface_culling = true }, -- roof
{ name = "default_tree.png", backface_culling = true }, -- corner wood
}
}))
minetest.register_node("commoditymarket_fantasy:caravan_market_5", create_caravan_def({
tiles = {
{ name = "commoditymarket_door_wood.png", backface_culling = true }, -- door
{ name = "default_pine_wood.png", backface_culling = true }, -- base wood
{ name = "default_chest_lock.png", backface_culling = true }, -- wheel sides
{ name = "default_chest_top.png", backface_culling = true }, -- wheel tyre
{ name = "default_furnace_top.png", backface_culling = true }, -- roof
{ name = "default_wood.png", backface_culling = true }, -- corner wood
}
}))
local caravan_protect = minetest.settings:get_bool("commoditymarket_protect_caravan_market", true)
local on_blast
if caravan_protect then
on_blast = function() end
end
-- This one doesn't delete itself, server admins can place a permanent instance of it that way. Maybe inside towns next to bigger stationary markets.
minetest.register_node("commoditymarket_fantasy:caravan_market_permanent", {
description = caravan_def.description,
_doc_items_longdesc = caravan_def.long_description,
_doc_items_usagehelp = usage_help,
drawtype = "mesh",
mesh = "commoditymarket_wagon.obj",
tiles = {
{ name = "commoditymarket_door_wood.png", backface_culling = true }, -- door
{ name = "default_wood.png", backface_culling = true }, -- base wood
{ name = "default_fence_rail_wood.png", backface_culling = true }, -- wheel sides
{ name = "default_coal_block.png", backface_culling = true }, -- wheel tyre
{ name = "commoditymarket_shingles_wood.png", backface_culling = true }, -- roof
{ name = "default_junglewood.png", backface_culling = true }, -- corner wood
},
collision_box = {
type = "fixed",
fixed = {
{-0.75, -0.5, -1.25, 0.75, 1.5, 1.25},
},
},
selection_box = {
type = "fixed",
fixed = {
{-0.75, -0.5, -1.25, 0.75, 1.5, 1.25},
},
},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("caravan", clicker:get_player_name())
end,
can_dig = function(pos, player)
return not caravan_protect or minetest.check_player_privs(player, "protection_bypass")
end,
on_blast = on_blast,
})
-- is a 5x3 area centered around pos clear of obstruction and has usable ground?
local is_suitable_caravan_space = function(pos, facedir)
local x_dim = 2
local z_dim = 2
local dir = minetest.facedir_to_dir(facedir)
if dir.x ~= 0 then
z_dim = 1
elseif dir.z ~= 0 then
x_dim = 1
end
-- walkable ground?
for x = pos.x - x_dim, pos.x + x_dim, 1 do
for z = pos.z - z_dim, pos.z + z_dim, 1 do
local node = minetest.get_node({x=x, y=pos.y-1, z=z})
local node_def = minetest.registered_nodes[node.name]
if node_def == nil or node_def.walkable ~= true then return false end
end
end
-- buildable_to in the rest?
for y = pos.y, pos.y+2, 1 do
for x = pos.x - x_dim, pos.x + x_dim, 1 do
for z = pos.z - z_dim, pos.z + z_dim, 1 do
local node = minetest.get_node({x=x, y=y, z=z})
local node_def = minetest.registered_nodes[node.name]
if node_def == nil or node_def.buildable_to ~= true then return false end
end
end
end
return true
end
minetest.register_node("commoditymarket_fantasy:caravan_post", {
description = S("Trading Post"),
_long_items_longdesc = S("This post signals passing caravan traders that customers can be found here, and signals to customers that caravan traders can be found here. If no caravan is present, right-click to summon one."),
_doc_items_usagehelp = S("The trader's caravan requires a suitable open space next to the trading post for it to arrive, and takes some time to arrive after being summoned. The post gives a countdown to the caravan's arrival when moused over."),
tiles = {"commoditymarket_sign.png^[transformR90", "commoditymarket_sign.png^[transformR270",
"commoditymarket_sign.png^commoditymarket_caravan_sign.png", "commoditymarket_sign.png^commoditymarket_caravan_sign.png^[transformFX",
"commoditymarket_sign_post.png", "commoditymarket_sign_post.png"},
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
inventory_image = "commoditymarket_caravan_sign_inventory.png",
paramtype= "light",
paramtype2 = "facedir",
drawtype = "nodebox",
node_box = {
type = "fixed",
fixed = {
{-0.125,-0.5,-0.5,0.125,2.0625,-0.25},
{-0.0625,1.4375,-0.25,0.0625,2.0,0.5},
},
},
on_construct = function(pos)
local timer = minetest.get_node_timer(pos)
timer:start(1.0)
end,
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
local timer = minetest.get_node_timer(pos)
timer:start(1.0)
end,
on_timer = function(pos, elapsed)
local node = minetest.get_node(pos)
local meta = minetest.get_meta(pos)
if node.name ~= "commoditymarket_fantasy:caravan_post" then
return -- the node was removed
end
local facedir = node.param2
local dir = minetest.facedir_to_dir(facedir)
local target = vector.add(pos, vector.multiply(dir,3))
local target_node = minetest.get_node(target)
if target_node.name:sub(1,string.len("commoditymarket_fantasy:caravan_market")) == "commoditymarket_fantasy:caravan_market" then
-- It's already here somehow, shut down timer.
meta:set_string("infotext", "")
meta:set_float("wait_time", 0)
return
end
local is_suitable_space = is_suitable_caravan_space(target, facedir)
if not is_suitable_space then
meta:set_string("infotext", S("Indicated parking area isn't suitable.\nA 5x3 open space with solid ground\nis required for a caravan."))
meta:set_float("wait_time", 0)
local timer = minetest.get_node_timer(pos)
timer:start(1.0)
return
end
local wait_time = (meta:get_float("wait_time") or 0) + elapsed
meta:set_float("wait_time", wait_time)
if wait_time < time_until_caravan then
meta:set_string("infotext", S("Caravan summoned\nETA: @1 seconds.", math.floor(time_until_caravan - wait_time)))
local timer = minetest.get_node_timer(pos)
timer:start(1.0)
return
end
-- spawn the caravan. We've already established that the target pos is clear.
minetest.set_node(target, {name="commoditymarket_fantasy:caravan_market_"..math.random(1,5), param2=facedir})
minetest.sound_play("commoditymarket_register_opened", {
pos = target,
gain = 1.0, -- default
max_hear_distance = 32, -- default, uses an euclidean metric
})
local timer = minetest.get_node_timer(target)
timer:start(dwell_time)
meta:set_string("infotext", "")
meta:set_float("wait_time", 0)
end,
})
end
-------------------------------------------------------------------------------
-- "Goblin Exchange"
if minetest.settings:get_bool("commoditymarket_enable_goblin_market", true) then
local goblin_def = {
description = S("Goblin Exchange"),
long_description = S("One does not usually associate Goblins with the sort of sophistication that running a market requires. Usually one just associates Goblins with savagery and violence. But they understand the principle of tit-for-tat exchange, and if approached correctly they actually respect the concepts of ownership and debt. However, for some peculiar reason they understand this concept in the context of coal lumps. Goblins deal in the standard coal lump as their form of currency, conceptually divided into 100 coal centilumps (though Goblin brokers prefer to \"keep the change\" when giving back actual coal lumps)."),
currency = {
["default:coal_lump"] = 100
},
currency_symbol = "¢", --"\u{00A2}" cent symbol
inventory_limit = 1000,
--sell_limit =, -- no sell limit
}
commoditymarket.register_market("goblin", goblin_def)
local goblin_protect = minetest.settings:get_bool("commoditymarket_protect_goblin_market", true)
local on_blast
if goblin_protect then
on_blast = function() end
end
minetest.register_node("commoditymarket_fantasy:goblin_market", {
description = goblin_def.description,
_doc_items_longdesc = goblin_def.long_description,
_doc_items_usagehelp = usage_help,
tiles = {"default_chest_top.png^(default_coal_block.png^[opacity:128)","default_chest_top.png^(default_coal_block.png^[opacity:128)",
"default_chest_side.png^(default_coal_block.png^[opacity:128)","default_chest_side.png^(default_coal_block.png^[opacity:128)",
"commoditymarket_empty_shelf.png^(default_coal_block.png^[opacity:128)","default_chest_side.png^(default_coal_block.png^[opacity:128)^commoditymarket_goblin.png",},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_wood_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("goblin", clicker:get_player_name())
end,
can_dig = function(pos, player)
return not goblin_protect or minetest.check_player_privs(player, "protection_bypass")
end,
on_blast = on_blast,
})
end
--------------------------------------------------------------------------------
if minetest.settings:get_bool("commoditymarket_enable_under_market", true) then
local undermarket_def = {
description = S("Undermarket"),
long_description = S("Deep in the bowels of the world, below even the goblin-infested warrens and ancient delvings of the dwarves, dark and mysterious beings once dwelt. A few still linger to this day, and facilitate barter for those brave souls willing to travel in their lost realms. The Undermarket uses Mese chips ('₥') as a currency - twenty chips to the Mese fragment. Though traders are loathe to physically break Mese crystals up into units that small, as it renders it useless for other purposes."),
currency = {
["default:mese"] = 9*9*20,
["default:mese_crystal"] = 9*20,
["default:mese_crystal_fragment"] = 20
},
currency_symbol = "", --"\u{20A5}" mill sign
inventory_limit = 10000,
--sell_limit =, -- no sell limit
}
commoditymarket.register_market("under", undermarket_def)
local under_protect = minetest.settings:get_bool("commoditymarket_protect_under_market", true)
local on_blast
if under_protect then
on_blast = function() end
end
minetest.register_node("commoditymarket_fantasy:under_market", {
description = undermarket_def.description,
_doc_items_longdesc = undermarket_def.long_description,
_doc_items_usagehelp = usage_help,
tiles = {"commoditymarket_under_top.png","commoditymarket_under_top.png",
"commoditymarket_under.png","commoditymarket_under.png","commoditymarket_under.png","commoditymarket_under.png"},
paramtype2 = "facedir",
is_ground_content = false,
groups = {choppy = 2, oddly_breakable_by_hand = 1,},
sounds = default.node_sound_stone_defaults(),
on_rightclick = function(pos, node, clicker, itemstack, pointed_thing)
commoditymarket.show_market("under", clicker:get_player_name())
end,
can_dig = function(pos, player)
return not under_protect or minetest.check_player_privs(player, "protection_bypass")
end,
on_blast = on_blast,
})
end
------------------------------------------------------------------

View File

@ -1,42 +0,0 @@
# textdomain: commoditymarket_fantasy
### init.lua ###
A gold ingot is far too valuable to use as a basic unit of value, so it has become common practice to divide the standard gold bar into @1 small disks to make trade easier.=
At this time of day the King's Market is closed.=
At this time of day the Night Market is closed.=
Caravan summoned@nETA: @1 seconds.=
Deep in the bowels of the world, below even the goblin-infested warrens and ancient delvings of the dwarves, dark and mysterious beings once dwelt. A few still linger to this day, and facilitate barter for those brave souls willing to travel in their lost realms. The Undermarket uses Mese chips ('₥') as a currency - twenty chips to the Mese fragment. Though traders are loathe to physically break Mese crystals up into units that small, as it renders it useless for other purposes.=
Goblin Exchange=
Gold Coins=
Gold coins can be deposited and withdrawn from markets that accept them as currency. These markets can make change if you have @1 coins and would like them back in ingot form again.=
Indicated parking area isn't suitable.@nA 5x3 open space with solid ground@nis required for a caravan.=
King's Market=
Night Market=
One does not usually associate Goblins with the sort of sophistication that running a market requires. Usually one just associates Goblins with savagery and violence. But they understand the principle of tit-for-tat exchange, and if approached correctly they actually respect the concepts of ownership and debt. However, for some peculiar reason they understand this concept in the context of coal lumps. Goblins deal in the standard coal lump as their form of currency, conceptually divided into 100 coal centilumps (though Goblin brokers prefer to "keep the change" when giving back actual coal lumps).=
Right-click on this to open the market interface.=
Right-click to summon a trader's caravan=
The largest and most accessible market for the common man, the King's Market uses gold coins as its medium of exchange (or the equivalent in gold ingots - @1 coins to the ingot). However, as a respectable institution of the surface world, the King's Market operates only during the hours of daylight. The purchase and sale of swords and explosives is prohibited in the King's Market. Gold coins are represented by a '☼' symbol.=
The trader's caravan requires a suitable open space next to the trading post for it to arrive, and takes some time to arrive after being summoned. The post gives a countdown to the caravan's arrival when moused over.=
This post signals passing caravan traders that customers can be found here, and signals to customers that caravan traders can be found here. If no caravan is present, right-click to summon one.=
Trader's Caravan=
Trading Post=
Undermarket=
Unlike most markets that have well-known fixed locations that travelers congregate to, the network of Trader's Caravans is fluid and dynamic in their locations. A Trader's Caravan can show up anywhere, make modest trades, and then be gone the next time you visit them. These caravans accept gold and gold coins as a currency (one gold ingot to @1 gold coins exchange rate). Any reasonably-wealthy person can create a signpost marking a location where Trader's Caravans will make a stop.=
When the sun sets and the stalls of the King's Market close, other vendors are just waking up to share their wares. The Night Market is not as voluminous as the King's Market but accepts a wider range of wares. It accepts the same gold coinage of the realm, @1 coins to the gold ingot.=

View File

@ -1,153 +0,0 @@
local goblin_enabled = minetest.settings:get_bool("commoditymarket_enable_goblin_market")
local under_enabled = minetest.settings:get_bool("commoditymarket_enable_under_market")
local goblin_prob = tonumber(minetest.settings:get("commoditymarket_goblin_market_dungeon_prob")) or 0.25
local under_prob = tonumber(minetest.settings:get("commoditymarket_under_market_dungeon_prob")) or 0.1
local goblin_max = tonumber(minetest.settings:get("commoditymarket_goblin_market_dungeon_max")) or 100
local goblin_min = tonumber(minetest.settings:get("commoditymarket_goblin_market_dungeon_min")) or -400
local under_max = tonumber(minetest.settings:get("commoditymarket_under_market_dungeon_max")) or -500
local under_min = tonumber(minetest.settings:get("commoditymarket_under_market_dungeon_min")) or -31000
local bad_goblin_range = goblin_min >= goblin_max
local bad_under_range = under_min >= under_max
if bad_goblin_range then
minetest.log("error", "[commoditymarket] Goblin market dungeon generation range has a higher minimum y than maximum y")
end
if bad_under_range then
minetest.log("error", "[commoditymarket] Undermarket dungeon generation range has a higher minimum y than maximum y")
end
local gen_goblin = goblin_enabled and goblin_prob > 0 and not bad_goblin_range
local gen_under = under_enabled and under_prob > 0 and not bad_under_range
if not (gen_goblin or gen_under) then
return
end
-------------------------------------------------------
-- The following is shamelessly copied from dungeon_loot and tweaked for placing markets instead of chests
--Licensed under the MIT License (MIT) Copyright (C) 2017 sfan5
minetest.set_gen_notify({dungeon = true, temple = true})
local function noise3d_integer(noise, pos)
return math.abs(math.floor(noise:get_3d(pos) * 0x7fffffff))
end
local is_wall = function(node)
return node.name ~= "air" and node.name ~= "ignore"
end
local function find_walls(cpos)
local dirs = {{x=1, z=0}, {x=-1, z=0}, {x=0, z=1}, {x=0, z=-1}}
local get_node = minetest.get_node
local ret = {}
local mindist = {x=0, z=0}
local min = function(a, b) return a ~= 0 and math.min(a, b) or b end
for _, dir in ipairs(dirs) do
for i = 1, 9 do -- 9 = max room size / 2
local pos = vector.add(cpos, {x=dir.x*i, y=0, z=dir.z*i})
-- continue in that direction until we find a wall-like node
local node = get_node(pos)
if is_wall(node) then
local front_below = vector.subtract(pos, {x=dir.x, y=1, z=dir.z})
local above = vector.add(pos, {x=0, y=1, z=0})
-- check that it:
--- is at least 2 nodes high (not a staircase)
--- has a floor
if is_wall(get_node(front_below)) and is_wall(get_node(above)) then
pos = vector.subtract(pos, {x=dir.x, y=0, z=dir.z}) -- move goblin markets one node away from the wall
table.insert(ret, {pos = pos, facing = {x=-dir.x, y=0, z=-dir.z}})
if dir.z == 0 then
mindist.x = min(mindist.x, i-1)
else
mindist.z = min(mindist.z, i-1)
end
end
-- abort even if it wasn't a wall cause something is in the way
break
end
end
end
return {
walls = ret,
size = {x=mindist.x*2, z=mindist.z*2},
cpos = cpos,
}
end
minetest.register_on_generated(function(minp, maxp, blockseed)
local min_y = minp.y
local max_y = maxp.y
local gen_goblin_range = gen_goblin and not (min_y > goblin_max or max_y < goblin_min)
local gen_under_range = gen_under and not (min_y > under_max or max_y < under_min)
if not (gen_goblin_range or gen_under_range) then
-- out of both ranges
return
end
local gennotify = minetest.get_mapgen_object("gennotify")
local poslist = gennotify["dungeon"] or {}
for _, entry in ipairs(gennotify["temple"] or {}) do
table.insert(poslist, entry)
end
if #poslist == 0 then return end
local noise = minetest.get_perlin(151994, 4, 0.5, 1)
local rand = PcgRandom(noise3d_integer(noise, poslist[1]))
local rooms = {}
-- process at most 8 rooms to keep runtime of this predictable
local num_process = math.min(#poslist, 8)
for i = 1, num_process do
local room = find_walls(poslist[i])
-- skip small rooms and everything that doesn't at least have 3 walls
if math.min(room.size.x, room.size.z) >= 4 and #room.walls >= 3 then
table.insert(rooms, room)
end
end
if #rooms == 0 then return end
if gen_under_range and rand:next(0, 2147483647)/2147483647 < under_prob then
-- choose a random room
local room = rooms[rand:next(1, #rooms)]
local under_loc = room.cpos
-- put undermarkets in the center of the room
if minetest.get_node(under_loc).name == "air"
and is_wall(vector.subtract(under_loc, {x=0, y=1, z=0})) then
minetest.add_node(under_loc, {name="commoditymarket:under_market"})
end
end
if gen_goblin_range and rand:next(0, 2147483647)/2147483647 < goblin_prob then
-- choose a random room
local room = rooms[rand:next(1, #rooms)]
-- choose place somewhere in front of any of the walls
local wall = room.walls[rand:next(1, #room.walls)]
local v, vi -- vector / axis that runs alongside the wall
if wall.facing.x ~= 0 then
v, vi = {x=0, y=0, z=1}, "z"
else
v, vi = {x=1, y=0, z=0}, "x"
end
local marketpos = vector.add(wall.pos, wall.facing)
local off = rand:next(-room.size[vi]/2 + 1, room.size[vi]/2 - 1)
marketpos = vector.add(marketpos, vector.multiply(v, off))
if minetest.get_node(marketpos).name == "air" then
-- make it face inwards to the room
local facedir = minetest.dir_to_facedir(vector.multiply(wall.facing, -1))
minetest.add_node(marketpos, {name = "commoditymarket:goblin_market", param2 = facedir})
end
end
end)

View File

@ -1,4 +0,0 @@
name = commoditymarket_fantasy
description = Adds a number of fantasy-themed marketplaces
depends = commoditymarket, default
optional_depends = doc

View File

@ -1,852 +0,0 @@
# Blender v2.79 (sub 0) OBJ File: 'wagon.blend'
# www.blender.org
g Cube.004_Cube.012
v 0.234629 1.147573 -1.166732
v -0.234629 1.147573 -1.166732
v 0.234629 0.678315 -1.166732
v -0.234629 0.678315 -1.166732
v 0.731823 1.147573 -0.234629
v 0.731823 1.147573 0.234629
v 0.731823 0.678315 -0.234629
v 0.731823 0.678315 0.234629
v -0.731823 1.147573 0.234629
v -0.731823 1.147573 -0.234629
v -0.731823 0.678315 0.234629
v -0.731823 0.678315 -0.234629
v -0.259667 0.016670 1.168107
v 0.259668 0.016670 1.168107
v -0.259667 1.170112 1.168107
v 0.259668 1.170112 1.168107
vt 0.000000 0.533333
vt 0.000000 1.000000
vt 1.000000 1.000000
vt 1.000000 0.533333
vt -0.000000 0.533333
vt 1.000000 0.533333
vt 1.000000 1.000000
vt -0.000000 1.000000
vt 0.000000 0.533333
vt 1.000000 0.533333
vt 1.000000 1.000000
vt 0.000000 1.000000
vt 0.000000 0.000000
vt 1.000000 0.000000
vt 1.000000 1.000000
vt 0.000000 1.000000
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
s off
f 4/1/1 2/2/1 1/3/1 3/4/1
f 8/5/2 7/6/2 5/7/2 6/8/2
f 12/9/3 11/10/3 9/11/3 10/12/3
f 13/13/4 14/14/4 16/15/4 15/16/4
g Cube.003_Cube.011
v -0.720902 -0.096672 1.153442
v -0.720901 -0.096672 -1.153443
v -0.720902 1.200951 1.153442
v -0.720902 1.200951 -1.153443
v -0.509754 1.608755 -1.153443
v -0.509754 1.608755 1.153443
v -0.792992 1.200951 -1.268787
v -0.792992 1.200951 1.268787
v -0.560730 1.649535 -1.268787
v -0.560730 1.649535 1.268787
v -0.726391 1.178882 -1.268787
v -0.726391 1.178882 1.268787
v -0.509756 1.608756 -1.268787
v -0.509756 1.608756 1.268787
v -0.692620 0.037741 -1.018928
v -0.692620 0.163735 -1.089024
v -0.692620 -0.523026 -2.026881
v -0.692620 -0.397032 -2.096977
v -0.512394 0.037741 -1.018928
v -0.512394 0.163735 -1.089024
v -0.512394 -0.523026 -2.026881
v -0.512394 -0.397032 -2.096977
v -0.324406 0.582331 -1.134006
v -0.324406 0.654421 -1.134006
v -0.324406 0.582331 -1.441803
v -0.324406 0.654421 -1.441803
v 0.720901 -0.096672 -1.153443
v 0.720902 -0.096672 1.153442
v -0.000000 1.200951 1.153443
v 0.720902 1.200951 1.153442
v -0.000000 1.200951 -1.153443
v -0.000000 1.777673 -1.153443
v -0.000000 1.777673 1.153443
v 0.720902 1.200951 -1.153443
v 0.509754 1.608755 -1.153443
v 0.509754 1.608755 1.153443
v -0.000000 1.835345 1.268787
v -0.000000 1.835345 -1.268787
v 0.792992 1.200951 -1.268787
v 0.792992 1.200951 1.268787
v 0.560730 1.649535 -1.268787
v 0.560730 1.649535 1.268787
v -0.000000 1.777673 1.268787
v -0.000000 1.777673 -1.268787
v 0.726391 1.178882 -1.268787
v 0.726391 1.178882 1.268787
v 0.509756 1.608756 -1.268787
v 0.509756 1.608756 1.268787
v 0.692620 0.037741 -1.018928
v 0.692620 0.163735 -1.089024
v 0.692620 -0.523026 -2.026881
v 0.692620 -0.397032 -2.096977
v 0.512394 0.037741 -1.018928
v 0.512394 0.163735 -1.089024
v 0.512394 -0.523026 -2.026881
v 0.512394 -0.397032 -2.096977
v 0.324406 0.582331 -1.134006
v 0.324406 0.654421 -1.134006
v 0.324406 0.582331 -1.441803
v 0.324406 0.654421 -1.441803
vt -1.425823 3.001615
vt -1.425823 2.882734
vt -0.355896 2.882734
vt -0.355896 3.001615
vt 0.375155 -3.373241
vt 0.375155 -2.579424
vt -3.809447 -2.579426
vt -3.809447 -3.373241
vt 0.375154 -1.693858
vt -3.809447 -1.693859
vt 0.381165 -0.154615
vt 4.565710 -0.154608
vt 4.565710 -0.038908
vt 0.381165 -0.038908
vt -0.235621 2.375158
vt -0.235621 3.208172
vt -0.341187 3.187105
vt -0.349886 2.393338
vt 2.764791 -0.422899
vt 2.764791 -1.255914
vt 2.879056 -1.237734
vt 2.870357 -0.443966
vt -1.425823 3.804063
vt -0.451704 3.804062
vt -0.481619 3.894339
vt -1.367183 3.894337
vt -0.451699 3.798052
vt -1.425823 3.798051
vt -1.367184 3.707779
vt -0.481614 3.707775
vt 2.764791 -2.832267
vt 2.764791 -3.070028
vt 4.666879 -3.070028
vt 4.666879 -2.832265
vt 2.885066 -1.553966
vt 2.885066 -1.791727
vt 3.182267 -1.791727
vt 3.182267 -1.553966
vt 4.666881 -2.826254
vt 4.666881 -2.588493
vt 2.764791 -2.588493
vt 2.764791 -2.826255
vt 3.182267 -1.547955
vt 3.182267 -1.310194
vt 2.885066 -1.310194
vt 2.885066 -1.547955
vt 1.902838 4.586475
vt 1.902838 4.883677
vt 0.000747 4.883678
vt 0.000747 4.586475
vt 1.902835 4.283262
vt 1.902835 4.580463
vt 0.000747 4.580466
vt 0.000747 4.283262
vt 2.764791 -0.298008
vt 2.764791 -0.416888
vt 3.272367 -0.416888
vt 3.272367 -0.298008
vt -1.425823 2.375158
vt -0.355896 2.375158
vt -0.355896 3.509191
vt -1.425823 3.509191
vt -3.809448 2.369149
vt -3.809448 -0.008467
vt -0.005263 -0.008466
vt -0.005264 2.369148
vt 0.375154 -0.014477
vt -3.809448 -0.014477
vt -3.809448 -0.808292
vt 0.375154 -0.808291
vt 0.381165 -0.160630
vt 0.381165 -0.276331
vt 4.565711 -0.276331
vt 4.565711 -0.160625
vt 2.764791 -2.094939
vt 2.879056 -2.076759
vt 2.870358 -1.282992
vt 2.764791 -1.261924
vt -0.115346 3.208172
vt -0.220912 3.187105
vt -0.229611 2.393338
vt -0.115346 2.375158
vt -0.451703 3.515201
vt -0.510342 3.605475
vt -1.395907 3.605478
vt -1.425823 3.515201
vt -1.425823 3.701766
vt -1.395907 3.611488
vt -0.510338 3.611492
vt -0.451699 3.701765
vt 2.764791 -2.338711
vt 4.666883 -2.338711
vt 4.666883 -2.100949
vt 2.764791 -2.100949
vt 3.122827 -2.094939
vt 3.122827 -1.797738
vt 2.885066 -1.797738
vt 2.885066 -2.094939
vt 4.666883 -2.344721
vt 2.764791 -2.344721
vt 2.764791 -2.582482
vt 4.666883 -2.582483
vt -0.349886 3.214182
vt -0.052684 3.214182
vt -0.052684 3.451943
vt -0.349886 3.451943
vt 4.666882 -3.076038
vt 2.764791 -3.076038
vt 2.764791 -3.373240
vt 4.666883 -3.373241
vt 3.810932 4.580466
vt 1.908845 4.580463
vt 1.908845 4.283263
vt 3.810934 4.283262
vt 2.885066 -1.304184
vt 3.392642 -1.304184
vt 3.392642 -1.185303
vt 2.885066 -1.185303
vt 0.061193 0.029274
vt 2.438806 0.029274
vt 2.438807 2.169126
vt 1.250000 2.169127
vt 0.061193 2.169127
vt 2.438741 0.029448
vt 2.438741 2.169301
vt 6.242925 2.169302
vt 6.242926 0.029448
vt 2.090614 2.841618
vt 1.250000 2.169127
vt 0.409386 2.841618
vt 0.061192 2.169127
vt 1.250000 3.120173
vt 1.250000 3.120173
vt 2.438532 0.029230
vt 6.242717 0.029228
vt 6.242717 2.169081
vt 2.438534 2.169083
vt 0.409386 2.841618
vt 2.438807 2.169127
vt 2.090614 2.841617
vt 0.061193 0.029273
vt 2.438807 0.029273
vn 0.0000 0.0000 -1.0000
vn 0.8930 -0.4500 -0.0000
vn 0.3145 -0.9492 0.0000
vn -0.3145 -0.9492 0.0000
vn 0.0000 0.0000 1.0000
vn -1.0000 0.0000 0.0000
vn 0.0000 -0.4862 -0.8739
vn 1.0000 -0.0000 0.0000
vn 0.0000 0.4862 0.8739
vn 0.0000 -0.8739 0.4862
vn 0.0000 0.8739 -0.4862
vn 0.0000 1.0000 0.0000
vn 0.0000 -1.0000 0.0000
vn -0.8930 -0.4500 -0.0000
s off
f 41/17/5 42/18/5 76/19/5 75/20/5
f 27/21/6 29/22/6 30/23/6 28/24/6
f 29/22/7 60/25/7 59/26/7 30/23/7
f 24/27/8 23/28/8 27/29/8 28/30/8
f 23/31/5 25/32/5 29/33/5 27/34/5
f 26/35/9 24/36/9 28/37/9 30/38/9
f 25/39/5 54/40/5 60/41/5 29/42/5
f 53/43/9 26/44/9 30/45/9 59/46/9
f 31/47/10 32/48/10 34/49/10 33/50/10
f 33/51/11 34/52/11 38/53/11 37/54/11
f 37/55/12 38/56/12 36/57/12 35/58/12
f 35/59/13 36/60/13 32/61/13 31/62/13
f 33/63/14 37/64/14 35/65/14 31/66/14
f 38/67/15 34/68/15 32/69/15 36/70/15
f 39/71/10 40/72/10 42/73/10 41/74/10
f 76/19/16 42/18/16 40/75/16 74/76/16
f 41/17/17 75/20/17 73/77/17 39/78/17
f 44/79/17 17/80/17 18/81/17 43/82/17
f 61/83/18 62/84/18 64/85/18 63/86/18
f 63/86/8 64/85/8 59/26/8 60/25/8
f 56/87/7 62/88/7 61/89/7 55/90/7
f 55/91/5 61/92/5 63/93/5 57/94/5
f 58/95/9 64/96/9 62/97/9 56/98/9
f 57/99/5 63/100/5 60/101/5 54/102/5
f 53/103/9 59/104/9 64/105/9 58/106/9
f 65/107/12 67/108/12 68/109/12 66/110/12
f 67/111/11 71/112/11 72/113/11 68/114/11
f 71/115/10 69/116/10 70/117/10 72/118/10
f 69/119/13 65/120/13 66/121/13 70/122/13
f 67/123/14 65/124/14 69/125/14 71/126/14
f 72/127/15 70/128/15 66/129/15 68/130/15
f 73/131/12 75/132/12 76/133/12 74/134/12
f 43/135/5 18/136/5 20/137/5 47/138/5 50/139/5
f 17/140/10 19/141/10 20/142/10 18/143/10
f 47/138/5 20/137/5 21/144/5
f 45/145/9 22/146/9 19/147/9
f 47/138/5 21/144/5 48/148/5
f 45/145/9 49/149/9 22/146/9
f 44/150/12 43/151/12 50/152/12 46/153/12
f 47/138/5 51/154/5 50/139/5
f 45/145/9 46/155/9 52/156/9
f 47/138/5 48/148/5 51/154/5
f 45/145/9 52/156/9 49/149/9
f 45/145/9 19/147/9 17/157/9 44/158/9 46/155/9
g Cube.002_Cube.010
v -0.901127 0.047509 -0.576721
v -0.720902 0.047509 -0.576721
v -0.901127 0.268210 -1.109542
v -0.720902 0.268210 -1.109542
v -0.901127 -0.173193 -1.109542
v -0.720902 -0.173193 -1.109542
v -0.901127 -0.485312 -0.797423
v -0.720902 -0.485312 -0.797423
v -0.901127 -0.485312 -0.356020
v -0.720902 -0.485312 -0.356020
v -0.901127 -0.173193 -0.043900
v -0.720902 -0.173193 -0.043900
v -0.901127 0.268210 -0.043900
v -0.720902 0.268210 -0.043900
v -0.901127 0.580330 -0.356020
v -0.720902 0.580330 -0.356020
v -0.901127 0.580330 -0.797423
v -0.720902 0.580330 -0.797423
v -0.901127 0.047509 0.576721
v -0.720902 0.047509 0.576721
v -0.901127 0.268210 0.043900
v -0.720902 0.268210 0.043900
v -0.901127 -0.173193 0.043900
v -0.720902 -0.173193 0.043900
v -0.901127 -0.485312 0.356020
v -0.720902 -0.485312 0.356020
v -0.901127 -0.485312 0.797423
v -0.720902 -0.485312 0.797423
v -0.901127 -0.173193 1.109542
v -0.720902 -0.173193 1.109542
v -0.901127 0.268210 1.109542
v -0.720902 0.268210 1.109542
v -0.901127 0.580330 0.797423
v -0.720902 0.580330 0.797423
v -0.901127 0.580330 0.356020
v -0.720902 0.580330 0.356020
v 0.901127 0.047509 -0.576721
v 0.720902 0.047509 -0.576721
v 0.901127 0.268210 -1.109542
v 0.720902 0.268210 -1.109542
v 0.901127 -0.173193 -1.109542
v 0.720902 -0.173193 -1.109542
v 0.901127 -0.485312 -0.797423
v 0.720902 -0.485312 -0.797423
v 0.901127 -0.485312 -0.356020
v 0.720902 -0.485312 -0.356020
v 0.901127 -0.173193 -0.043900
v 0.720902 -0.173193 -0.043900
v 0.901127 0.268210 -0.043900
v 0.720902 0.268210 -0.043900
v 0.901127 0.580330 -0.356020
v 0.720902 0.580330 -0.356020
v 0.901127 0.580330 -0.797423
v 0.720902 0.580330 -0.797423
v 0.901127 0.047509 0.576721
v 0.720902 0.047509 0.576721
v 0.901127 0.268210 0.043900
v 0.720902 0.268210 0.043900
v 0.901127 -0.173193 0.043900
v 0.720902 -0.173193 0.043900
v 0.901127 -0.485312 0.356020
v 0.720902 -0.485312 0.356020
v 0.901127 -0.485312 0.797423
v 0.720902 -0.485312 0.797423
v 0.901127 -0.173193 1.109542
v 0.720902 -0.173193 1.109542
v 0.901127 0.268210 1.109542
v 0.720902 0.268210 1.109542
v 0.901127 0.580330 0.797423
v 0.720902 0.580330 0.797423
v 0.901127 0.580330 0.356020
v 0.720902 0.580330 0.356020
vt 0.499873 0.992213
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.499873 0.992213
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.499873 0.992213
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.499873 0.992213
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.499873 0.992213
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.499873 0.992213
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.499873 0.992213
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.499873 0.992213
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vt 0.085485 -0.008208
vt 0.914261 -0.008208
vt 0.914261 -0.008208
vt 0.085485 -0.008208
vn -1.0000 0.0000 0.0000
vn 1.0000 0.0000 0.0000
s off
f 77/159/19 79/160/19 81/161/19
f 78/162/20 82/163/20 80/164/20
f 77/159/19 81/165/19 83/166/19
f 78/162/20 84/167/20 82/168/20
f 77/159/19 83/169/19 85/170/19
f 78/162/20 86/171/20 84/172/20
f 77/159/19 85/173/19 87/174/19
f 78/162/20 88/175/20 86/176/20
f 77/159/19 87/177/19 89/178/19
f 78/162/20 90/179/20 88/180/20
f 77/159/19 89/181/19 91/182/19
f 78/162/20 92/183/20 90/184/20
f 77/159/19 91/185/19 93/186/19
f 78/162/20 94/187/20 92/188/20
f 77/159/19 93/189/19 79/190/19
f 78/162/20 80/191/20 94/192/20
f 95/193/19 97/194/19 99/195/19
f 96/196/20 100/197/20 98/198/20
f 95/193/19 99/199/19 101/200/19
f 96/196/20 102/201/20 100/202/20
f 95/193/19 101/203/19 103/204/19
f 96/196/20 104/205/20 102/206/20
f 95/193/19 103/207/19 105/208/19
f 96/196/20 106/209/20 104/210/20
f 95/193/19 105/211/19 107/212/19
f 96/196/20 108/213/20 106/214/20
f 95/193/19 107/215/19 109/216/19
f 96/196/20 110/217/20 108/218/20
f 95/193/19 109/219/19 111/220/19
f 96/196/20 112/221/20 110/222/20
f 95/193/19 111/223/19 97/224/19
f 96/196/20 98/225/20 112/226/20
f 113/227/20 117/228/20 115/229/20
f 114/230/19 116/231/19 118/232/19
f 113/227/20 119/233/20 117/234/20
f 114/230/19 118/235/19 120/236/19
f 113/227/20 121/237/20 119/238/20
f 114/230/19 120/239/19 122/240/19
f 113/227/20 123/241/20 121/242/20
f 114/230/19 122/243/19 124/244/19
f 113/227/20 125/245/20 123/246/20
f 114/230/19 124/247/19 126/248/19
f 113/227/20 127/249/20 125/250/20
f 114/230/19 126/251/19 128/252/19
f 113/227/20 129/253/20 127/254/20
f 114/230/19 128/255/19 130/256/19
f 113/227/20 115/257/20 129/258/20
f 114/230/19 130/259/19 116/260/19
f 131/261/20 135/262/20 133/263/20
f 132/264/19 134/265/19 136/266/19
f 131/261/20 137/267/20 135/268/20
f 132/264/19 136/269/19 138/270/19
f 131/261/20 139/271/20 137/272/20
f 132/264/19 138/273/19 140/274/19
f 131/261/20 141/275/20 139/276/20
f 132/264/19 140/277/19 142/278/19
f 131/261/20 143/279/20 141/280/20
f 132/264/19 142/281/19 144/282/19
f 131/261/20 145/283/20 143/284/20
f 132/264/19 144/285/19 146/286/19
f 131/261/20 147/287/20 145/288/20
f 132/264/19 146/289/19 148/290/19
f 131/261/20 133/291/20 147/292/20
f 132/264/19 148/293/19 134/294/19
g Cube.001_Cube.009
v -0.901127 0.268210 -1.109542
v -0.720902 0.268210 -1.109542
v -0.901127 -0.173193 -1.109542
v -0.720902 -0.173193 -1.109542
v -0.901127 -0.485312 -0.797423
v -0.720902 -0.485312 -0.797423
v -0.901127 -0.485312 -0.356020
v -0.720902 -0.485312 -0.356020
v -0.901127 -0.173193 -0.043900
v -0.720902 -0.173193 -0.043900
v -0.901127 0.268210 -0.043900
v -0.720902 0.268210 -0.043900
v -0.901127 0.580330 -0.356020
v -0.720902 0.580330 -0.356020
v -0.901127 0.580330 -0.797423
v -0.720902 0.580330 -0.797423
v -0.901127 0.268210 0.043900
v -0.720902 0.268210 0.043900
v -0.901127 -0.173193 0.043900
v -0.720902 -0.173193 0.043900
v -0.901127 -0.485312 0.356020
v -0.720902 -0.485312 0.356020
v -0.901127 -0.485312 0.797423
v -0.720902 -0.485312 0.797423
v -0.901127 -0.173193 1.109542
v -0.720902 -0.173193 1.109542
v -0.901127 0.268210 1.109542
v -0.720902 0.268210 1.109542
v -0.901127 0.580330 0.797423
v -0.720902 0.580330 0.797423
v -0.901127 0.580330 0.356020
v -0.720902 0.580330 0.356020
v 0.901127 0.268210 -1.109542
v 0.720902 0.268210 -1.109542
v 0.901127 -0.173193 -1.109542
v 0.720902 -0.173193 -1.109542
v 0.901127 -0.485312 -0.797423
v 0.720902 -0.485312 -0.797423
v 0.901127 -0.485312 -0.356020
v 0.720902 -0.485312 -0.356020
v 0.901127 -0.173193 -0.043900
v 0.720902 -0.173193 -0.043900
v 0.901127 0.268210 -0.043900
v 0.720902 0.268210 -0.043900
v 0.901127 0.580330 -0.356020
v 0.720902 0.580330 -0.356020
v 0.901127 0.580330 -0.797423
v 0.720902 0.580330 -0.797423
v 0.901127 0.268210 0.043900
v 0.720902 0.268210 0.043900
v 0.901127 -0.173193 0.043900
v 0.720902 -0.173193 0.043900
v 0.901127 -0.485312 0.356020
v 0.720902 -0.485312 0.356020
v 0.901127 -0.485312 0.797423
v 0.720902 -0.485312 0.797423
v 0.901127 -0.173193 1.109542
v 0.720902 -0.173193 1.109542
v 0.901127 0.268210 1.109542
v 0.720902 0.268210 1.109542
v 0.901127 0.580330 0.797423
v 0.720902 0.580330 0.797423
v 0.901127 0.580330 0.356020
v 0.720902 0.580330 0.356020
vt 0.279164 0.493799
vt 0.072560 0.493799
vt 0.072560 -0.012210
vt 0.279164 -0.012210
vt 0.072559 -0.518219
vt 0.279163 -0.518219
vt 0.072559 -1.024229
vt 0.279163 -1.024229
vt 0.072559 -1.530238
vt 0.279163 -1.530238
vt 0.279165 2.517837
vt 0.072561 2.517837
vt 0.072561 2.011827
vt 0.279165 2.011827
vt 0.072561 1.505818
vt 0.279165 1.505818
vt 0.072560 0.999809
vt 0.279165 0.999809
vt 0.694571 2.011827
vt 0.901175 2.011827
vt 0.901175 2.517837
vt 0.694571 2.517837
vt 0.694571 -1.530238
vt 0.901175 -1.530238
vt 0.901175 -1.024228
vt 0.694571 -1.024228
vt 0.901175 -0.518219
vt 0.694571 -0.518219
vt 0.901175 -0.012210
vt 0.694571 -0.012210
vt 0.901174 0.493799
vt 0.694570 0.493799
vt 0.901174 0.999809
vt 0.694570 0.999809
vt 0.901175 1.505817
vt 0.694570 1.505818
vt 0.279897 0.493799
vt 0.279898 -0.012210
vt 0.486502 -0.012210
vt 0.486501 0.493799
vt 0.279898 -0.518219
vt 0.486502 -0.518219
vt 0.279898 -1.024228
vt 0.486502 -1.024229
vt 0.279898 -1.530238
vt 0.486502 -1.530238
vt 0.279897 2.517837
vt 0.279897 2.011827
vt 0.486501 2.011827
vt 0.486501 2.517837
vt 0.279897 1.505818
vt 0.486501 1.505818
vt 0.279897 0.999809
vt 0.486501 0.999809
vt 0.693838 2.011827
vt 0.693838 2.517837
vt 0.487234 2.517837
vt 0.487234 2.011827
vt 0.693839 -1.530238
vt 0.693838 -1.024229
vt 0.487234 -1.024229
vt 0.487234 -1.530238
vt 0.693838 -0.518220
vt 0.487234 -0.518220
vt 0.693838 -0.012210
vt 0.487234 -0.012210
vt 0.693838 0.493800
vt 0.487234 0.493800
vt 0.693838 0.999809
vt 0.487234 0.999809
vt 0.693838 1.505818
vt 0.487234 1.505818
vn 0.0000 0.0000 -1.0000
vn -0.0000 -0.7071 -0.7071
vn -0.0000 -1.0000 -0.0000
vn -0.0000 -0.7071 0.7071
vn 0.0000 0.0000 1.0000
vn 0.0000 0.7071 0.7071
vn 0.0000 1.0000 0.0000
vn 0.0000 0.7071 -0.7071
s off
f 149/295/21 150/296/21 152/297/21 151/298/21
f 151/298/22 152/297/22 154/299/22 153/300/22
f 153/300/23 154/299/23 156/301/23 155/302/23
f 155/302/24 156/301/24 158/303/24 157/304/24
f 157/305/25 158/306/25 160/307/25 159/308/25
f 159/308/26 160/307/26 162/309/26 161/310/26
f 161/310/27 162/309/27 164/311/27 163/312/27
f 163/312/28 164/311/28 150/296/28 149/295/28
f 165/313/21 166/314/21 168/315/21 167/316/21
f 167/317/22 168/318/22 170/319/22 169/320/22
f 169/320/23 170/319/23 172/321/23 171/322/23
f 171/322/24 172/321/24 174/323/24 173/324/24
f 173/324/25 174/323/25 176/325/25 175/326/25
f 175/326/26 176/325/26 178/327/26 177/328/26
f 177/328/27 178/327/27 180/329/27 179/330/27
f 179/330/28 180/329/28 166/314/28 165/313/28
f 181/331/21 183/332/21 184/333/21 182/334/21
f 183/332/22 185/335/22 186/336/22 184/333/22
f 185/335/23 187/337/23 188/338/23 186/336/23
f 187/337/24 189/339/24 190/340/24 188/338/24
f 189/341/25 191/342/25 192/343/25 190/344/25
f 191/342/26 193/345/26 194/346/26 192/343/26
f 193/345/27 195/347/27 196/348/27 194/346/27
f 195/347/28 181/331/28 182/334/28 196/348/28
f 197/349/21 199/350/21 200/351/21 198/352/21
f 199/353/22 201/354/22 202/355/22 200/356/22
f 201/354/23 203/357/23 204/358/23 202/355/23
f 203/357/24 205/359/24 206/360/24 204/358/24
f 205/359/25 207/361/25 208/362/25 206/360/25
f 207/361/26 209/363/26 210/364/26 208/362/26
f 209/363/27 211/365/27 212/366/27 210/364/27
f 211/365/28 197/349/28 198/352/28 212/366/28
g Cube.000_Cube.008
v -0.792992 1.200951 -1.268787
v -0.792992 1.200951 1.268787
v -0.560730 1.649535 -1.268787
v -0.560730 1.649535 1.268787
v -0.000000 1.835345 1.268787
v -0.000000 1.835345 -1.268787
v 0.792992 1.200951 -1.268787
v 0.792992 1.200951 1.268787
v 0.560730 1.649535 -1.268787
v 0.560730 1.649535 1.268787
vt 3.173871 2.353881
vt -2.391578 2.353881
vt -2.391578 1.288979
vt 3.173871 1.288979
vt -2.391578 0.224077
vt 3.173871 0.224077
vt 3.173871 -1.905729
vt 3.173871 -0.840826
vt -2.391578 -0.840826
vt -2.391578 -1.905728
vn -0.8880 0.4598 0.0000
vn -0.3146 0.9492 0.0000
vn 0.8880 0.4598 0.0000
vn 0.3146 0.9492 0.0000
s off
f 213/367/29 214/368/29 216/369/29 215/370/29
f 215/370/30 216/369/30 217/371/30 218/372/30
f 219/373/31 221/374/31 222/375/31 220/376/31
f 221/374/32 218/372/32 217/371/32 222/375/32
g Cube.006_Cube.007
v -0.722861 -0.094866 1.158711
v -0.722861 1.240430 1.158711
v -0.722861 -0.094866 1.059710
v -0.722861 1.240430 1.059710
v -0.623860 -0.094866 1.158711
v -0.623860 1.240430 1.158711
v -0.725927 -0.094866 -1.158643
v -0.725927 1.240430 -1.158643
v -0.725927 -0.094866 -1.059642
v -0.725927 1.240430 -1.059642
v -0.626926 -0.094866 -1.158643
v -0.626926 1.240430 -1.158643
v 0.722861 -0.094866 1.158711
v 0.722861 1.240430 1.158711
v 0.722861 -0.094866 1.059710
v 0.722861 1.240430 1.059710
v 0.623860 -0.094866 1.158711
v 0.623860 1.240430 1.158711
v 0.725927 -0.094866 -1.158643
v 0.725927 1.240430 -1.158643
v 0.725927 -0.094866 -1.059642
v 0.725927 1.240430 -1.059642
v 0.626926 -0.094866 -1.158643
v 0.626926 1.240430 -1.158643
vt -0.339657 0.628705
vt 1.340090 0.628704
vt 1.340090 0.753243
vt -0.339657 0.753243
vt -0.339658 0.504167
vt 1.340090 0.504165
vt -0.339657 0.129512
vt -0.339657 0.004973
vt 1.340090 0.004973
vt 1.340090 0.129512
vt -0.339657 0.254051
vt 1.340090 0.254050
vt -0.339658 0.878300
vt -0.339658 0.753761
vt 1.340090 0.753762
vt 1.340090 0.878300
vt -0.339657 1.002839
vt 1.340090 1.002838
vt -0.339657 0.379109
vt 1.340090 0.379108
vt 1.340090 0.503647
vt -0.339657 0.503647
vt -0.339658 0.254571
vt 1.340090 0.254569
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
s off
f 223/377/33 224/378/33 226/379/33 225/380/33
f 227/381/34 228/382/34 224/378/34 223/377/34
f 229/383/33 231/384/33 232/385/33 230/386/33
f 233/387/35 229/383/35 230/386/35 234/388/35
f 235/389/36 237/390/36 238/391/36 236/392/36
f 239/393/34 235/389/34 236/392/34 240/394/34
f 241/395/36 242/396/36 244/397/36 243/398/36
f 245/399/35 246/400/35 242/396/35 241/395/35

View File

@ -1,15 +0,0 @@
This mod depends on [commoditymarket](https://github.com/FaceDeer/commoditymarket). It implements a number of fantasy-themed marketplaces where players can post buy and sell offers for various items, allowing for organic market forces to determine the relative values of the resources in a world.
* King's Market - a basic sort of "commoner's marketplace", only open during the day
* Night Market - the shadier side of commerce, only open during the night
* Trader's Caravan - a type of market that players can build and place themselves, with a small inventory capacity.
* Goblin Exchange - a strange marketplace that uses coal as a currency
* Undermarket - where dark powers make their trades, using Mese as a currency
![](screenshot.jpg)
All of these except for the Trader's Caravan are intended to be placed in specific locations by server administrators, they don't have crafting recipes.
An option exists to allow Goblin markets and Undermarkets to be automatically placed inside dungeons by world gen.
The [settlements](https://github.com/FaceDeer/settlements) mod scatters small settlements around on the surface of the world that can contain King's Markets and Night Markets.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

View File

@ -1,24 +0,0 @@
[Enable default markets]
commoditymarket_enable_kings_market (Enable King's Market) bool true
commoditymarket_enable_night_market (Enable Night Market) bool true
commoditymarket_enable_caravan_market (Enable Trader's Caravan) bool true
commoditymarket_enable_goblin_market (Enable Goblin Exchange) bool true
commoditymarket_enable_under_market (Enable Undermarket) bool true
[Market node protection]
commoditymarket_protect_kings_market (Protect King's Market node) bool true
commoditymarket_protect_night_market (Protect Night Market node) bool true
commoditymarket_protect_caravan_market (Protect permanent Caravan node) bool true
commoditymarket_protect_goblin_market (Protect Goblin Market node) bool true
commoditymarket_protect_under_market (Protect Undermarket node) bool true
[Currency denominations]
commoditymarket_coins_per_ingot (Coins per ingot) int 1000
[Dungeon market generation]
commoditymarket_goblin_market_dungeon_prob (Goblin market probability per dungeon mapblock) float 0.25 0 1
commoditymarket_goblin_market_dungeon_max (Upper y limit of goblin markets) int 100
commoditymarket_goblin_market_dungeon_min (Lower y limit of goblin markets) int -400
commoditymarket_under_market_dungeon_prob (Undermarket probability per dungeon mapblock) float 0.1 0 1
commoditymarket_under_market_dungeon_max (Upper y limit of undermarkets) int -500
commoditymarket_under_market_dungeon_min (Lower y limit of undermarkets) int -31000

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 759 B

View File

@ -1,11 +0,0 @@
commoditymarket_gold_coins.png - from https://commons.wikimedia.org/wiki/File:Farm-Fresh_coins.png, by FatCow under the CC-BY 3.0 license
commoditymarket_crown.png - from https://commons.wikimedia.org/wiki/File:Farm-Fresh_crown_gold.png by FatCow under the CC-BY 3.0 Unported license
commoditymarket_moon.png - from https://commons.wikimedia.org/wiki/File:Luneta08.svg by Arturo D. Castillo —Zoram.hakaan— under the CC-BY 3.0 Unported license
commoditymarket_goblin.png - cropped from the "goblins" mod, Copyright 2015 by Francisco "FreeLikeGNU" Athens Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)
commoditymarket_empty_shelf.png - from the moreblocks mod's "moreblocks_empty_shelf", under the zlib license by Hugo Locurcio and contributors
commoditymarket_shingles_wood.png is cottages_homedecor_shingles_wood.png from the cottages mod by VanessaE (CC-by-SA 3.0)
commoditymarket_trade.png, commoditymarket_trade_flag.png, commoditymarket_under.png, and commoditymarket_under_top were created by FaceDeer and released under the CC0 public domain license
commoditymarket_sign_post.png and commoditymarket_sign.png are made from elements of default_tree.png and default_aspen_wood.png
commoditymarket_door_wood.png is doors_door_wood.png from the doors mod

View File

@ -19,4 +19,4 @@ For developers, `craftguide` also has a [modding API](https://github.com/minetes
Love this mod? Donations are appreciated: https://www.paypal.me/jpg84240
![Preview2](https://i.imgur.com/w7KMS9G.png)
![Preview2](https://content.minetest.net/uploads/wAGr5rE3fI.png)

View File

@ -7,13 +7,15 @@ local searches = {}
local recipes_cache = {}
local usages_cache = {}
local fuel_cache = {}
local toolrepair
local progressive_mode = core.settings:get_bool("craftguide_progressive_mode")
local sfinv_only = core.settings:get_bool("craftguide_sfinv_only") and rawget(_G, "sfinv")
local progressive_mode = core.settings:get_bool "craftguide_progressive_mode"
local sfinv_only = core.settings:get_bool "craftguide_sfinv_only" and rawget(_G, "sfinv")
local autocache = core.settings:get_bool "craftguide_autocache"
local http = core.request_http_api()
local storage = core.get_mod_storage()
local singleplayer = core.is_singleplayer()
local reg_items = core.registered_items
local reg_tools = core.registered_tools
@ -41,7 +43,7 @@ local get_player_info = core.get_player_information
local on_receive_fields = core.register_on_player_receive_fields
local ESC = core.formspec_escape
local S = core.get_translator("craftguide")
local S = core.get_translator "craftguide"
local ES = function(...)
return ESC(S(...))
@ -56,12 +58,14 @@ local fmt, find, gmatch, match, sub, split, upper, lower =
string.sub, string.split, string.upper, string.lower
local min, max, floor, ceil = math.min, math.max, math.floor, math.ceil
local pairs, next, type, tostring, io = pairs, next, type, tostring, io
local pairs, next, type, tostring, unpack = pairs, next, type, tostring, unpack
local vec_add, vec_mul = vector.add, vector.multiply
local ROWS = 9
local FORMSPEC_MINIMAL_VERSION = 3
local ROWS = 9
local LINES = sfinv_only and 5 or 9
local IPP = ROWS * LINES
local IPP = ROWS * LINES
local WH_LIMIT = 8
local XOFFSET = sfinv_only and 3.83 or 11.2
@ -79,6 +83,7 @@ local PNG = {
fire_anim = "craftguide_fire_anim.png",
book = "craftguide_book.png",
sign = "craftguide_sign.png",
nothing = "craftguide_no.png",
selected = "craftguide_selected.png",
furnace_anim = "craftguide_furnace_anim.png",
@ -96,7 +101,7 @@ local FMT = {
tooltip = "tooltip[%f,%f;%f,%f;%s]",
item_image = "item_image[%f,%f;%f,%f;%s]",
image_button = "image_button[%f,%f;%f,%f;%s;%s;%s]",
animated_image = "animated_image[%f,%f;%f,%f;%s:%u,%u]",
animated_image = "animated_image[%f,%f;%f,%f;;%s;%u;%u]",
item_image_button = "item_image_button[%f,%f;%f,%f;%s;%s;%s]",
arrow = "image_button[%f,%f;0.8,0.8;%s;%s;;;false;%s]",
}
@ -106,6 +111,20 @@ local function get_fs_version(name)
return info and info.formspec_version or 1
end
local function outdated(name)
local fs = fmt([[
size[6.6,1.3]
image[0,0;1,1;%s]
label[1,0;%s]
button_exit[2.8,0.8;1,1;;OK]
]],
PNG.book,
"Your Minetest client is outdated.\n" ..
"Get the latest version on minetest.net to use the Crafting Guide.")
return show_formspec(name, "craftguide", fs)
end
local function mul_elem(elem, n)
local fstr, elems = "", {}
@ -120,6 +139,8 @@ end
craftguide.group_stereotypes = {
dye = "dye:white",
wool = "wool:white",
wood = "default:wood",
tree = "default:tree",
coal = "default:coal_lump",
vessel = "vessels:glass_bottle",
flower = "flowers:dandelion_yellow",
@ -127,12 +148,48 @@ craftguide.group_stereotypes = {
mesecon_conductor_craftable = "mesecons:wire_00000000_off",
}
local group_names = {
coal = S"Any coal",
wool = S"Any wool",
wood = S"Any wood planks",
sand = S"Any sand",
stick = S"Any stick",
stone = S"Any kind of stone block",
tree = S"Any tree",
vessel = S"Any vessel",
["color_red,flower"] = S"Any red flower",
["color_blue,flower"] = S"Any blue flower",
["color_black,flower"] = S"Any black flower",
["color_white,flower"] = S"Any white flower",
["color_green,flower"] = S"Any green flower",
["color_orange,flower"] = S"Any orange flower",
["color_yellow,flower"] = S"Any yellow flower",
["color_violet,flower"] = S"Any violet flower",
["color_red,dye"] = S"Any red dye",
["color_blue,dye"] = S"Any blue dye",
["color_grey,dye"] = S"Any grey dye",
["color_pink,dye"] = S"Any pink dye",
["color_cyan,dye"] = S"Any cyan dye",
["color_black,dye"] = S"Any black dye",
["color_white,dye"] = S"Any white dye",
["color_brown,dye"] = S"Any brown dye",
["color_green,dye"] = S"Any green dye",
["color_orange,dye"] = S"Any orange dye",
["color_yellow,dye"] = S"Any yellow dye",
["color_violet,dye"] = S"Any violet dye",
["color_magenta,dye"] = S"Any magenta dye",
["color_dark_grey,dye"] = S"Any dark grey dye",
["color_dark_green,dye"] = S"Any dark green dye",
}
local function err(str)
return log("error", str)
end
local function msg(name, str)
return chat_send(name, fmt("[craftguide] %s", clr("#f00", str)))
return chat_send(name, fmt("[craftguide] %s", str))
end
local function is_str(x)
@ -194,6 +251,7 @@ local function table_eq(T1, T2)
local function recurse(t1, t2)
if type(t1) ~= type(t2) then return end
if not is_table(t1) then
return t1 == t2
end
@ -274,7 +332,7 @@ local craft_types = {}
function craftguide.register_craft_type(name, def)
if not true_str(name) then
return err"craftguide.register_craft_type(): name missing"
return err "craftguide.register_craft_type(): name missing"
end
if not is_str(def.description) then
@ -293,8 +351,9 @@ function craftguide.register_craft(def)
if true_str(def.url) then
if not http then
return err"No HTTP support for this mod. " ..
"Add it to the `secure.http_mods` or `secure.trusted_mods` setting."
return err(fmt([[craftguide.register_craft(): Unable to reach %s.
No HTTP support for this mod: add it to the `secure.http_mods` or
`secure.trusted_mods` setting.]], def.url))
end
http.fetch({url = def.url}, function(result)
@ -310,7 +369,7 @@ function craftguide.register_craft(def)
end
if not is_table(def) or not next(def) then
return err"craftguide.register_craft(): craft definition missing"
return err "craftguide.register_craft(): craft definition missing"
end
if #def > 1 then
@ -326,7 +385,7 @@ function craftguide.register_craft(def)
end
if not true_str(def.output) then
return err"craftguide.register_craft(): output missing"
return err "craftguide.register_craft(): output missing"
end
if not is_table(def.items) then
@ -396,9 +455,9 @@ local recipe_filters = {}
function craftguide.add_recipe_filter(name, f)
if not true_str(name) then
return err"craftguide.add_recipe_filter(): name missing"
return err "craftguide.add_recipe_filter(): name missing"
elseif not is_func(f) then
return err"craftguide.add_recipe_filter(): function missing"
return err "craftguide.add_recipe_filter(): function missing"
end
recipe_filters[name] = f
@ -406,9 +465,9 @@ end
function craftguide.set_recipe_filter(name, f)
if not is_str(name) then
return err"craftguide.set_recipe_filter(): name missing"
return err "craftguide.set_recipe_filter(): name missing"
elseif not is_func(f) then
return err"craftguide.set_recipe_filter(): function missing"
return err "craftguide.set_recipe_filter(): function missing"
end
recipe_filters = {[name] = f}
@ -434,9 +493,9 @@ local search_filters = {}
function craftguide.add_search_filter(name, f)
if not true_str(name) then
return err"craftguide.add_search_filter(): name missing"
return err "craftguide.add_search_filter(): name missing"
elseif not is_func(f) then
return err"craftguide.add_search_filter(): function missing"
return err "craftguide.add_search_filter(): function missing"
end
search_filters[name] = f
@ -465,6 +524,7 @@ end
local function item_in_recipe(item, recipe)
local clean_item = reg_aliases[item] or item
for _, recipe_item in pairs(recipe.items) do
local clean_recipe_item = reg_aliases[recipe_item] or recipe_item
if clean_recipe_item == clean_item then
@ -602,7 +662,7 @@ local function get_recipes(item, data, player)
local no_usages = not usages or #usages == 0
return not no_recipes and recipes or nil,
not no_usages and usages or nil
not no_usages and usages or nil
end
local function groups_to_items(groups, get_all)
@ -651,6 +711,10 @@ local function is_fav(data)
return fav, i
end
local function check_newline(def)
return def and def.description and find(def.description, "\n")
end
local function get_desc(name)
if sub(name, 1, 1) == "_" then
name = sub(name, 2)
@ -667,15 +731,20 @@ local function get_tooltip(name, info)
local tooltip
if info.groups then
local groupstr, c = {}, 0
sort(info.groups)
tooltip = group_names[concat(info.groups, ",")]
for i = 1, #info.groups do
c = c + 1
groupstr[c] = clr("#ff0", info.groups[i])
if not tooltip then
local groupstr, c = {}, 0
for i = 1, #info.groups do
c = c + 1
groupstr[c] = clr("#ff0", info.groups[i])
end
groupstr = concat(groupstr, ", ")
tooltip = S("Any item belonging to the group(s): @1", groupstr)
end
groupstr = concat(groupstr, ", ")
tooltip = S("Any item belonging to the group(s): @1", groupstr)
else
tooltip = get_desc(name)
end
@ -728,60 +797,50 @@ local function get_output_fs(data, fs, L)
end
local pos_x = L.rightest + L.btn_size + 0.1
local pos_y = YOFFSET + (sfinv_only and 0.25 or -0.45)
local pos_y = YOFFSET + (sfinv_only and 0.25 or -0.45) + L.spacing
if data.fs_version >= 3 and sub(icon, 1, 18) == "craftguide_furnace" then
if sub(icon, 1, 18) == "craftguide_furnace" then
fs[#fs + 1] = fmt(FMT.animated_image,
pos_x, pos_y + L.spacing, 0.5, 0.5, PNG.furnace_anim, 8, 180)
pos_x, pos_y, 0.5, 0.5, PNG.furnace_anim, 8, 180)
else
fs[#fs + 1] = fmt(FMT.image, pos_x, pos_y + L.spacing, 0.5, 0.5, icon)
fs[#fs + 1] = fmt(FMT.image, pos_x, pos_y, 0.5, 0.5, icon)
end
local tooltip = custom_recipe and custom_recipe.description or
L.shapeless and S"Shapeless" or S"Cooking"
fs[#fs + 1] = fmt(FMT.tooltip, pos_x, pos_y + L.spacing, 0.5, 0.5, ESC(tooltip))
fs[#fs + 1] = fmt(FMT.tooltip, pos_x, pos_y, 0.5, 0.5, ESC(tooltip))
end
local arrow_X = L.rightest + (L._btn_size or 1.1)
local output_X = arrow_X + 0.9
local Y = YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing
fs[#fs + 1] = fmt(FMT.image,
arrow_X, YOFFSET + (sfinv_only and 0.9 or 0.2) + L.spacing,
0.9, 0.7, PNG.arrow)
fs[#fs + 1] = fmt(FMT.image, arrow_X, Y + 0.2, 0.9, 0.7, PNG.arrow)
if L.recipe.type == "fuel" then
if data.fs_version >= 3 then
fs[#fs + 1] = fmt(FMT.animated_image,
output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing,
1.1, 1.1, PNG.fire_anim, 8, 180)
else
fs[#fs + 1] = fmt(FMT.image,
output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing,
1.1, 1.1, PNG.fire)
end
fs[#fs + 1] = fmt(FMT.animated_image, output_X, Y, 1.1, 1.1, PNG.fire_anim, 8, 180)
else
local item = L.recipe.output
item = clean_name(item)
local name = match(item, "%S*")
if data.fs_version >= 3 then
fs[#fs + 1] = fmt(FMT.image,
output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing,
1.1, 1.1, PNG.selected)
end
fs[#fs + 1] = fmt(FMT.image, output_X, Y, 1.1, 1.1, PNG.selected)
local _name = sfinv_only and name or fmt("_%s", name)
fs[#fs + 1] = fmt("item_image_button[%f,%f;%f,%f;%s;%s;%s]",
output_X, YOFFSET + (sfinv_only and 0.7 or 0) + L.spacing,
1.1, 1.1, item, _name, "")
output_X, Y, 1.1, 1.1, item, _name, "")
local def = reg_items[name]
local infos = {
unknown = not reg_items[name] or nil,
unknown = not def or nil,
burntime = fuel_cache[name],
repair = repairable(name),
rarity = L.rarity,
newline = check_newline(def),
}
if next(infos) then
@ -793,15 +852,9 @@ local function get_output_fs(data, fs, L)
output_X + 1, YOFFSET + (sfinv_only and 0.7 or 0.1) + L.spacing,
0.6, 0.4, PNG.arrow)
if data.fs_version >= 3 then
fs[#fs + 1] = fmt(FMT.animated_image,
output_X + 1.6, YOFFSET + (sfinv_only and 0.55 or 0) + L.spacing,
0.6, 0.6, PNG.fire_anim, 8, 180)
else
fs[#fs + 1] = fmt(FMT.image,
output_X + 1.6, YOFFSET + (sfinv_only and 0.55 or 0) + L.spacing,
0.6, 0.6, PNG.fire)
end
fs[#fs + 1] = fmt(FMT.animated_image,
output_X + 1.6, YOFFSET + (sfinv_only and 0.55 or 0) + L.spacing,
0.6, 0.6, PNG.fire_anim, 8, 180)
end
end
end
@ -856,9 +909,9 @@ local function get_grid_fs(data, fs, rcp, spacing)
_btn_size = btn_size
X = (btn_size * ((i - 1) % width) + XOFFSET -
(sfinv_only and 2.83 or 0.5)) * (0.83 - (x_y / 5))
(sfinv_only and 2.83 or 0)) * (0.83 - (x_y / 5))
Y = (btn_size * floor((i - 1) / width) +
(sfinv_only and 5.81 or 5.5) + x_y) * (0.86 - (x_y / 5))
(sfinv_only and 5.81 or 3.92) + x_y) * (0.86 - (x_y / 5))
end
if X > rightest then
@ -885,21 +938,24 @@ local function get_grid_fs(data, fs, rcp, spacing)
end
end
if data.fs_version >= 3 and not large_recipe then
fs[#fs + 1] = fmt(FMT.image,
X, Y + (sfinv_only and 0.7 or 0), btn_size, btn_size, PNG.selected)
Y = Y + (sfinv_only and 0.7 or 0)
if not large_recipe then
fs[#fs + 1] = fmt(FMT.image, X, Y, btn_size, btn_size, PNG.selected)
end
fs[#fs + 1] = fmt(FMT.item_image_button,
X, Y + (sfinv_only and 0.7 or 0),
btn_size, btn_size, item, item, ESC(label))
X, Y, btn_size, btn_size, item, item, label)
local def = reg_items[name]
local infos = {
unknown = not reg_items[name] or nil,
unknown = not def or nil,
groups = groups,
burntime = fuel_cache[name],
cooktime = cooktime,
replace = replace,
newline = check_newline(def),
}
if next(infos) then
@ -923,19 +979,17 @@ local function get_grid_fs(data, fs, rcp, spacing)
end
local function get_panels(data, fs)
local start_y = data.fs_version >= 3 and (sfinv_only and 0.33 or 0) or 0.33
local start_y = sfinv_only and 0.33 or 0
local panels = {
{dat = data.usages or {}, height = 3.5},
{dat = data.recipes or {}, height = 3.5},
}
if data.fs_version >= 3 and not sfinv_only then
panels.favs = {dat = {}, height = 2.19}
elseif sfinv_only then
panels = data.show_usages and
{{dat = data.usages}} or {{dat = data.recipes}}
if not sfinv_only then
panels.favs = {height = 2.19}
else
panels = data.show_usages and {{dat = data.usages}} or {{dat = data.recipes}}
end
for k, v in pairs(panels) do
@ -946,7 +1000,7 @@ local function get_panels(data, fs)
fs[#fs + 1] = fmt("background9[8.1,%f;6.6,%f;%s;false;%d]",
-0.2 + spacing, v.height, PNG.bg_full, 10)
if data.fs_version >= 3 and k == 2 then
if k == 2 then
local fav = is_fav(data)
local nfavs = #data.favs
@ -966,7 +1020,7 @@ local function get_panels(data, fs)
end
end
local rn = #v.dat
local rn = v.dat and #v.dat or -1
local _rn = tostring(rn)
local xu = tostring(data.unum) .. _rn
local xr = tostring(data.rnum) .. _rn
@ -974,10 +1028,16 @@ local function get_panels(data, fs)
xr = max(-0.3, -((#xr - 3) * 0.05))
local is_recipe = sfinv_only and not data.show_usages or k == 2
local lbl
local lbl = ""
if not sfinv_only and rn == 0 then
lbl = clr("#f00", is_recipe and ES"No recipes" or ES"No usages")
local X = XOFFSET - 0.7
local Y = YOFFSET - 0.4 + spacing
fs[#fs + 1] = fmt(FMT.image, X, Y, 2, 2, PNG.nothing)
fs[#fs + 1] = fmt(FMT.tooltip,
X, Y, 2, 2, is_recipe and ES"No recipes" or ES"No usages")
elseif (not sfinv_only and is_recipe) or
(sfinv_only and not data.show_usages) then
@ -1003,39 +1063,31 @@ local function get_panels(data, fs)
local x_arrow = XOFFSET + (sfinv_only and 1.7 or 1)
local y_arrow = YOFFSET + (sfinv_only and 3.3 or 1.4 + spacing)
if data.fs_version >= 3 then
fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2),
x_arrow + (is_recipe and xr or xu), y_arrow,
PNG.prev, prev_name, "",
x_arrow + 1.8, y_arrow, PNG.next, next_name, "")
else
fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2),
x_arrow + (is_recipe and xr or xu),
y_arrow, PNG.prev, prev_name, PNG.prev_hover,
x_arrow + 1.8, y_arrow, PNG.next, next_name, PNG.next_hover)
end
fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2),
x_arrow + (is_recipe and xr or xu), y_arrow,
PNG.prev, prev_name, "",
x_arrow + 1.8, y_arrow, PNG.next, next_name, "")
end
local rcp = is_recipe and v.dat[data.rnum] or v.dat[data.unum]
local rcp = v.dat and (is_recipe and v.dat[data.rnum] or v.dat[data.unum])
if rcp then
get_grid_fs(data, fs, rcp, spacing)
end
if k == "favs" and data.fs_version >= 3 and not sfinv_only then
if k == "favs" and not sfinv_only then
fs[#fs + 1] = fmt(FMT.label, 8.3, spacing - 0.1, ES"Bookmarks")
for i = 1, #data.favs do
local item = data.favs[i]
local X = 7.85 + (i - 0.5)
local Y = spacing + 0.45
if data.query_item == item then
fs[#fs + 1] = fmt(FMT.image,
7.85 + (i - 0.5), spacing + 0.45,
1.1, 1.1, PNG.selected)
fs[#fs + 1] = fmt(FMT.image, X, Y, 1.1, 1.1, PNG.selected)
end
fs[#fs + 1] = fmt(FMT.item_image_button,
7.85 + (i - 0.5), spacing + 0.45,
1.1, 1.1, item, item, "")
X, Y, 1.1, 1.1, item, item, "")
end
end
end
@ -1049,72 +1101,57 @@ local function make_fs(data)
no_prepend[]
bgcolor[#0000]
]],
ROWS + (data.query_item and 6.7 or 0) - 1.2, LINES - 0.3)
9 + (data.query_item and 6.7 or 0) - 1.2, LINES - 0.3)
if not sfinv_only then
fs[#fs + 1] = fmt("background9[-0.15,-0.2;%f,%f;%s;false;%d]",
ROWS - 0.9, LINES + 0.4, PNG.bg_full, 10)
9 - 0.9, LINES + 0.4, PNG.bg_full, 10)
end
fs[#fs + 1] = fmt([[
style[filter;border=false]
field[0.4,0.2;2.5,1;filter;;%s]
field_close_on_enter[filter;false]
box[0,0;2.4,0.6;#ffffff25]
box[0,0;2.4,0.6;#bababa25]
]],
ESC(data.filter))
if data.fs_version >= 3 then
fs[#fs + 1] = fmt([[
style_type[image_button;border=false]
style_type[item_image_button;border=false;bgimg_hovered=%s;bgimg_pressed=%s]
style[search;fgimg=%s;fgimg_hovered=%s]
style[clear;fgimg=%s;fgimg_hovered=%s]
style[prev_page;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]
style[next_page;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]
]],
PNG.selected, PNG.selected,
PNG.search, PNG.search_hover,
PNG.clear, PNG.clear_hover,
PNG.prev, PNG.prev_hover, PNG.prev_hover,
PNG.next, PNG.next_hover, PNG.next_hover)
fs[#fs + 1] = fmt([[
style_type[image_button;border=false]
style_type[item_image_button;border=false;bgimg_hovered=%s;bgimg_pressed=%s]
style[search;fgimg=%s;fgimg_hovered=%s]
style[clear;fgimg=%s;fgimg_hovered=%s]
style[prev_page;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]
style[next_page;fgimg=%s;fgimg_hovered=%s;fgimg_pressed=%s]
]],
PNG.selected, PNG.selected,
PNG.search, PNG.search_hover,
PNG.clear, PNG.clear_hover,
PNG.prev, PNG.prev_hover, PNG.prev_hover,
PNG.next, PNG.next_hover, PNG.next_hover)
fs[#fs + 1] = fmt(mul_elem(FMT.image_button, 4),
sfinv_only and 2.6 or 2.54, -0.06, 0.85, 0.85, "", "search", "",
sfinv_only and 3.3 or 3.25, -0.06, 0.85, 0.85, "", "clear", "",
sfinv_only and 5.45 or (ROWS * 6.83) / 11, -0.06, 0.85, 0.85, "", "prev_page", "",
sfinv_only and 7.2 or (ROWS * 8.75) / 11, -0.06, 0.85, 0.85, "", "next_page", "")
else
fs[#fs + 1] = fmt([[
image_button[%f,-0.06;0.85,0.85;%s;search;;;false;%s]
image_button[%f,-0.06;0.85,0.85;%s;clear;;;false;%s]
]],
sfinv_only and 2.6 or 2.54, PNG.search, PNG.search_hover,
sfinv_only and 3.3 or 3.25, PNG.clear, PNG.clear_hover)
fs[#fs + 1] = fmt(mul_elem(FMT.arrow, 2),
sfinv_only and 5.45 or (ROWS * 6.83) / 11, -0.05,
PNG.prev, "prev_page", PNG.prev_hover,
sfinv_only and 7.2 or (ROWS * 8.75) / 11, -0.05,
PNG.next, "next_page", PNG.next_hover)
end
fs[#fs + 1] = fmt(mul_elem(FMT.image_button, 4),
sfinv_only and 2.6 or 2.54, -0.06, 0.85, 0.85, "", "search", "",
sfinv_only and 3.3 or 3.25, -0.06, 0.85, 0.85, "", "clear", "",
sfinv_only and 5.45 or (9 * 6.83) / 11, -0.06, 0.85, 0.85, "", "prev_page", "",
sfinv_only and 7.2 or (9 * 8.75) / 11, -0.06, 0.85, 0.85, "", "next_page", "")
data.pagemax = max(1, ceil(#data.items / IPP))
fs[#fs + 1] = fmt("label[%f,%f;%s / %u]",
sfinv_only and 6.35 or (ROWS * 7.85) / 11,
sfinv_only and 6.35 or (9 * 7.85) / 11,
0.06, clr("#ff0", data.pagenum), data.pagemax)
if #data.items == 0 then
local no_item = S"No item to show"
local pos = ROWS / 3
local no_item = ES"No item to show"
local pos = 3
if next(recipe_filters) and #init_items > 0 and data.filter == "" then
no_item = S"Collect items to reveal more recipes"
no_item = ES"Collect items to reveal more recipes"
pos = pos - 1
end
fs[#fs + 1] = fmt(FMT.label, pos, 2, ESC(no_item))
fs[#fs + 1] = fmt(FMT.label, pos, 2, no_item)
end
local first_item = (data.pagenum - 1) * IPP
@ -1125,18 +1162,15 @@ local function make_fs(data)
local X = i % ROWS
local Y = (i % IPP - X) / ROWS + 1
X = X - (X * (sfinv_only and 0.12 or 0.14)) - 0.05
Y = Y - (Y * 0.1) - 0.1
if data.fs_version >= 3 and data.query_item == item then
fs[#fs + 1] = fmt(FMT.image,
X - (X * (sfinv_only and 0.12 or 0.14)) - 0.05,
Y - (Y * 0.1) - 0.1,
1, 1, PNG.selected)
if data.query_item == item then
fs[#fs + 1] = fmt(FMT.image, X, Y, 1, 1, PNG.selected)
end
fs[#fs + 1] = fmt("item_image_button[%f,%f;%f,%f;%s;%s_inv;]",
X - (X * (sfinv_only and 0.12 or 0.14)) - 0.05,
Y - (Y * 0.1) - 0.1,
1, 1, item, item)
X, Y, 1, 1, item, item)
end
if (data.recipes and #data.recipes > 0) or (data.usages and #data.usages > 0) then
@ -1157,12 +1191,12 @@ end
craftguide.register_craft_type("digging", {
description = ES"Digging",
icon = "default_tool_steelpick.png",
icon = "craftguide_steelpick.png",
})
craftguide.register_craft_type("digging_chance", {
description = ES"Digging Chance",
icon = "default_tool_mesepick.png",
icon = "craftguide_mesepick.png",
})
local function search(data)
@ -1399,8 +1433,7 @@ local function handle_drops_table(name, drop)
end
end
local function register_drops(name, def)
local drop = def.drop
local function register_drops(name, drop)
local dstack = ItemStack(drop)
if not dstack:is_empty() and dstack:get_name() ~= name then
@ -1458,59 +1491,46 @@ local function show_item(def)
def.description and def.description ~= ""
end
local function tablelen(t)
local c = 0
for _ in pairs(t) do
c = c + 1
end
return c
end
local function get_init_items()
local ic, it, last_str = 0, tablelen(reg_items), ""
local hash = {}
local init_items_bak = storage:get "init_items"
local function iop(str)
io.write(("\b \b"):rep(#last_str))
io.write(str)
io.flush()
last_str = str
end
if autocache == false and init_items_bak then
init_items = dslz(init_items_bak)
fuel_cache = dslz(storage:get "fuel_cache")
usages_cache = dslz(storage:get "usages_cache")
recipes_cache = dslz(storage:get "recipes_cache")
else
print "[craftguide] Caching data (this may take a while)"
local hash = {}
local full_char, empty_char = "#", " "
for name, def in pairs(reg_items) do
if show_item(def) then
if not fuel_cache[name] then
cache_fuel(name)
end
for name, def in pairs(reg_items) do
ic = ic + 1
local percent, bar, len = (ic * 100) / it, "[", 20
if not recipes_cache[name] then
cache_recipes(name)
end
for i = 1, len do
bar = bar .. (i <= percent / (100 / len) and full_char or empty_char)
end
cache_usages(name)
register_drops(name, def.drop)
iop(fmt("[craftguide] Caching data %s %u%%\r", bar .. "]", percent))
if show_item(def) then
if not fuel_cache[name] then
cache_fuel(name)
end
if not recipes_cache[name] then
cache_recipes(name)
end
cache_usages(name)
register_drops(name, def)
if name ~= "" and recipes_cache[name] or usages_cache[name] then
init_items[#init_items + 1] = name
hash[name] = true
if name ~= "" and recipes_cache[name] or usages_cache[name] then
init_items[#init_items + 1] = name
hash[name] = true
end
end
end
end
handle_aliases(hash)
sort(init_items)
handle_aliases(hash)
sort(init_items)
storage:set_string("init_items", slz(init_items))
storage:set_string("fuel_cache", slz(fuel_cache))
storage:set_string("usages_cache", slz(usages_cache))
storage:set_string("recipes_cache", slz(recipes_cache))
end
if http and true_str(craftguide.export_url) then
local post_data = {
@ -1524,8 +1544,6 @@ local function get_init_items()
post_data = write_json(post_data),
}
end
print()
end
local function init_data(name)
@ -1556,12 +1574,15 @@ on_mods_loaded(get_init_items)
on_joinplayer(function(player)
local name = player:get_player_name()
init_data(name)
if pdata[name].fs_version < FORMSPEC_MINIMAL_VERSION then
outdated(name)
end
end)
local function fields(player, _f)
local name = player:get_player_name()
local data = pdata[name]
--print(dump(_f))
if _f.clear then
reset_data(data)
@ -1651,19 +1672,18 @@ end
if sfinv_only then
sfinv.register_page("craftguide:craftguide", {
title = S"Guide",
title = S"Craft Guide",
is_in_nav = function(self, player, context)
local name = player:get_player_name()
return get_fs_version(name) >= 2
return get_fs_version(name) >= FORMSPEC_MINIMAL_VERSION
end,
get = function(self, player, context)
local name = player:get_player_name()
local data = pdata[name]
local formspec = make_fs(data)
return sfinv.make_formspec(player, context, formspec)
return sfinv.make_formspec(player, context, make_fs(data))
end,
on_enter = function(self, player, context)
@ -1691,9 +1711,8 @@ else
local name = user:get_player_name()
local data = pdata[name]
if data.fs_version == 1 then
return msg(name, "Your Minetest client is outdated. " ..
"Get the latest version on minetest.net to use the Crafting Guide.")
if data.fs_version < FORMSPEC_MINIMAL_VERSION then
return outdated(name)
end
if next(recipe_filters) then
@ -1724,7 +1743,12 @@ else
paramtype = "light",
paramtype2 = "wallmounted",
sunlight_propagates = true,
groups = {oddly_breakable_by_hand = 1, flammable = 3},
groups = {
choppy = 1,
attached_node = 1,
oddly_breakable_by_hand = 1,
flammable = 3,
},
node_box = {
type = "wallmounted",
wall_top = {-0.5, 0.4375, -0.5, 0.5, 0.5, 0.5},
@ -1757,7 +1781,7 @@ else
core.register_craft{
output = "craftguide:sign",
type = "shapeless",
recipe = {"signs:sign"}
recipe = {"default:sign_wall_wood"}
}
core.register_craft{
@ -1769,7 +1793,7 @@ else
if rawget(_G, "sfinv_buttons") then
sfinv_buttons.register_button("craftguide", {
title = S"Crafting Guide",
tooltip = S"Shows a list of available crafting recipes, cooking recipes and fuels",
tooltip = S"Shows a list of available crafting recipes",
image = PNG.book,
action = function(player)
on_use(player)
@ -1779,7 +1803,6 @@ else
end
if progressive_mode then
local PLAYERS = {}
local POLL_FREQ = 0.25
local HUD_TIMER_MAX = 1.5
@ -1839,11 +1862,7 @@ if progressive_mode then
return filtered
end
local item_lists = {
"main",
"craft",
"craftpreview",
}
local item_lists = {"main", "craft", "craftpreview"}
local function get_inv_items(player)
local inv = player:get_inventory()
@ -1870,6 +1889,34 @@ if progressive_mode then
return inv_items
end
local function init_hud(player, data)
data.hud = {
bg = player:hud_add{
hud_elem_type = "image",
position = {x = 0.78, y = 1},
alignment = {x = 1, y = 1},
scale = {x = 370, y = 112},
text = PNG.bg,
},
book = player:hud_add{
hud_elem_type = "image",
position = {x = 0.79, y = 1.02},
alignment = {x = 1, y = 1},
scale = {x = 4, y = 4},
text = PNG.book,
},
text = player:hud_add{
hud_elem_type = "text",
position = {x = 0.84, y = 1.04},
alignment = {x = 1, y = 1},
number = 0xffffff,
text = "",
},
}
end
local function show_hud_success(player, data)
-- It'd better to have an engine function `hud_move` to only need
-- 2 calls for the notification's back and forth.
@ -1917,8 +1964,9 @@ if progressive_mode then
-- Workaround. Need an engine call to detect when the contents of
-- the player inventory changed, instead.
local function poll_new_items()
for i = 1, #PLAYERS do
local player = PLAYERS[i]
local players = get_players()
for i = 1, #players do
local player = players[i]
local name = player:get_player_name()
local data = pdata[name]
@ -1951,12 +1999,13 @@ if progressive_mode then
poll_new_items()
globalstep(function()
for i = 1, #PLAYERS do
local player = PLAYERS[i]
local players = get_players()
for i = 1, #players do
local player = players[i]
local name = player:get_player_name()
local data = pdata[name]
if data.show_hud ~= nil then
if data.show_hud ~= nil and singleplayer then
show_hud_success(player, data)
end
end
@ -1965,46 +2014,19 @@ if progressive_mode then
craftguide.add_recipe_filter("Default progressive filter", progressive_filter)
on_joinplayer(function(player)
PLAYERS = get_players()
local name = player:get_player_name()
local data = pdata[name]
local meta = player:get_meta()
data.inv_items = dslz(meta:get_string("inv_items")) or {}
data.known_recipes = dslz(meta:get_string("known_recipes")) or 0
data.inv_items = dslz(meta:get_string "inv_items") or {}
data.known_recipes = dslz(meta:get_string "known_recipes") or 0
data.hud = {
bg = player:hud_add{
hud_elem_type = "image",
position = {x = 0.78, y = 1},
alignment = {x = 1, y = 1},
scale = {x = 370, y = 112},
text = PNG.bg,
},
book = player:hud_add{
hud_elem_type = "image",
position = {x = 0.79, y = 1.02},
alignment = {x = 1, y = 1},
scale = {x = 4, y = 4},
text = PNG.book,
},
text = player:hud_add{
hud_elem_type = "text",
position = {x = 0.84, y = 1.04},
alignment = {x = 1, y = 1},
number = 0xffffff,
text = "",
},
}
if singleplayer then
init_hud(player, data)
end
end)
local to_save = {
"inv_items",
"known_recipes",
}
local to_save = {"inv_items", "known_recipes"}
local function save_meta(player)
local meta = player:get_meta()
@ -2017,14 +2039,12 @@ if progressive_mode then
end
end
on_leaveplayer(function(player)
PLAYERS = get_players()
save_meta(player)
end)
on_leaveplayer(save_meta)
on_shutdown(function()
for i = 1, #PLAYERS do
local player = PLAYERS[i]
local players = get_players()
for i = 1, #players do
local player = players[i]
save_meta(player)
end
end)
@ -2037,7 +2057,7 @@ end)
function craftguide.show(name, item, show_usages)
if not true_str(name)then
return err"craftguide.show(): player name missing"
return err "craftguide.show(): player name missing"
end
local data = pdata[name]
@ -2052,13 +2072,11 @@ function craftguide.show(name, item, show_usages)
if not recipes and not usages then
if not recipes_cache[item] and not usages_cache[item] then
return false, msg(name, fmt("%s: %s",
S"No recipe or usage for this item",
get_desc(item)))
S"No recipe or usage for this item", get_desc(item)))
end
return false, msg(name, fmt("%s: %s",
S"You don't know a recipe or usage for this item",
get_desc(item)))
S"You don't know a recipe or usage for this item", get_desc(item)))
end
data.query_item = item

View File

@ -3,17 +3,58 @@
Craft Guide=Rezeptbuch
Crafting Guide=Rezeptbuch
Crafting Guide Sign=Rezepttafel
Bookmarks=Lesezeichen
Usage @1 of @2=Verwendung @1 von @2
Recipe @1 of @2=Rezept @1 von @2
No recipes=Keine Rezepte
No usages=Keine Verwendungen
Burning time: @1=Brennzeit: @1
Cooking time: @1=Kochzeit: @1
Replaced by @1 on smelting=Ersetzt durch @1 beim Schmelzen
Replaced by @1 on burning=Ersetzt durch @1 beim Brennen
Replaced by @1 on crafting=Ersetzt durch @1 beim Fertigen
Repairable by step of @1=Reparierbar um @1
Any item belonging to the group(s): @1=Beliebiger Gegenstand aus Gruppe(n): @1
Any black dye=Beliebiger schwarzer Farbstoff
Any black flower=Beliebige schwarze Blume
Any blue dye=Beliebiger blauer Farbstoff
Any blue flower=Beliebige blaue Blume
Any brown dye=Beliebiger brauner Farbstoff
Any coal=Beliebige Kohle
Any cyan dye=Beliebiger türkiser Farbstoff
Any dark green dye=Beliebiger dunkelgrüner Farbstoff
Any dark grey dye=Beliebiger dunkelgrauer Farbstoff
Any green dye=Beliebiger grüner Farbstoff
Any green flower=Beliebige grüne Blume
Any grey dye=Beliebiger grauer Farbstoff
Any kind of stone block=Beliebiger Steinblock
Any magenta dye=Beliebiger magenta Farbstoff
Any orange dye=Beliebiger orange Farbstoff
Any orange flower=Beliebige orange Blume
Any pink dye=Beliebiger rosa Farbstoff
Any red dye=Beliebiger roter Farbstoff
Any red flower=Beliebige rote Blume
Any sand=Beliebiger Sand
Any stick=Beliebiger Stock
Any tree=Beliebiger Baum
Any vessel=Beliebiger Behälter
Any violet dye=Beliebiger violetter Farbstoff
Any violet flower=Beliebige violette Blume
Any white dye=Beliebiger weißer Farbstoff
Any white flower=Beliebige weiße Blume
Any wood planks=Beliebige Holzplanken
Any wool=Beliebige Wolle
Any yellow dye=Beliebiger gelber Farbstoff
Any yellow flower=Beliebige gelbe Blume
Recipe's too big to be displayed (@1x@2)=Rezept ist zu groß für die Anzeige (@1×@2)
Shapeless=Formlos
Cooking=Kochen
No item to show=Nichts anzuzeigen
Collect items to reveal more recipes=Gegenstände aufsammeln, um mehr Rezepte aufzudecken
Show recipe(s) of the pointed node=Rezept(e) des gezeigten Blocks anzeigen
No node pointed=Auf keinen Block gezeigt
You don't know a recipe for this node=Sie kennen kein Rezept für diesen Block
No recipe for this node=Kein Rezept für diesen Block
No node pointed=Auf keinem Block gezeigt
You don't know a recipe or usage for this item=Sie kennen kein Rezept und keine Verwendung für diesen Gegenstand
No recipe or usage for this item=Kein Rezept und keine Verwendung für diesen Gegenstand
Digging=Graben
Digging Chance=Grabechance
@1 of chance to drop=@1 Abwurfwahrscheinlichkeit

View File

@ -2,8 +2,12 @@
Craft Guide=Guide de recettes
Crafting Guide=Guide de recettes
Crafting Guide Sign=Guide de recettes
Bookmarks=Favoris
Usage @1 of @2=Usage @1 de @2
Recipe @1 of @2=Recette @1 de @2
No recipes=Pas de recettes
No usages=Pas d'usages
Burning time: @1=Temps de combustion : @1
Cooking time: @1=Temps de cuisson : @1
Replaced by @1 on smelting=Remplacé par @1 lors de la cuisson
@ -11,6 +15,37 @@ Replaced by @1 on burning=Remplacé par @1 lors de la combustion
Replaced by @1 on crafting=Remplacé par @1 lors de la fabrication
Repairable by step of @1=Réparable par étape de @1
Any item belonging to the group(s): @1=Tout item appartenant au(x) groupe(s) : @1
Any black dye=Quelconque colorant noir
Any black flower=Quelconque fleur noire
Any blue dye=Quelconque colorant bleu
Any blue flower=Quelconque fleur bleue
Any brown dye=Quelconque colorant marron
Any coal=Quelconque charbon
Any cyan dye=Quelconque colorant bleu ciel
Any dark green dye=Quelconque colorant vert foncé
Any dark grey dye=Quelconque colorant gris foncé
Any green dye=Quelconque colorant vert
Any green flower=Quelconque fleur verte
Any grey dye=Quelconque colorant gris
Any kind of stone block=Quelconque roche
Any magenta dye=Quelconque colorant magenta
Any orange dye=Quelconque colorant orange
Any orange flower=Quelconque fleur orange
Any pink dye=Quelconque colorant rose
Any red dye=Quelconque colorant rouge
Any red flower=Quelconque fleur rouge
Any sand=Quelconque sable
Any stick=Quelconque bâton
Any tree=Quelconque tronc d'arbre
Any vessel=Quelconque couvert
Any violet dye=Quelconque colorant violet
Any violet flower=Quelconque fleur violette
Any white dye=Quelconque colorant blanc
Any white flower=Quelconque fleur blanche
Any wood planks=Quelconques planches de bois
Any wool=Quelconque laine
Any yellow dye=Quelconque colorant jaune
Any yellow flower=Quelconque fleur jaune
Recipe's too big to be displayed (@1x@2)=La recette est trop grande pour être affichée (@1x@2)
Shapeless=Sans forme
Cooking=Cuisson
@ -18,5 +53,8 @@ No item to show=Aucun item à afficher
Collect items to reveal more recipes=Collecte des items pour révéler plus de recettes
Show recipe(s) of the pointed node=Affiche les recettes du bloc visé
No node pointed=Aucun bloc visé
You don't know a recipe for this node=Tu ne connais aucune recette pour ce bloc
No recipe for this node=Aucune recette pour ce bloc
You don't know a recipe or usage for this item=Tu ne connais aucune recette pour ce bloc
No recipe or usage for this item=Aucune recette pour ce bloc
Digging=Destruction
Digging Chance=Destruction chanceuse
@1 of chance to drop=@1 de chance de tomber

View File

@ -2,25 +2,59 @@
Craft Guide=Guida di assemblaggio
Crafting Guide=Guida d'assemblaggio
Crafting Guide Sign=Cartello della guida d'assemblaggio
Crafting Guide Sign=Cartello della guida di assemblaggio
Bookmarks=Segnalibri
Usage @1 of @2=Utilizzo @1 di @2
Recipe @1 of @2=Ricetta @1 di @2
Burning time: @1=Tempo di bruciatura: @1
No recipes=Nessuna ricetta
No usages=Nessun utilizzo
Burning time: @1=Tempo di combustione: @1
Cooking time: @1=Tempo di cottura: @1
Replaced by @1 on smelting=Sostituito da @1 alla fusione
Replaced by @1 on burning=Sostituito da @1 alla bruciatura
Replaced by @1 on burning=Sostituito da @1 alla combustione
Replaced by @1 on crafting=Sostituito da @1 all'assemblaggio
Repairable by step of @1=Riparabile per passo di @1
Any item belonging to the group(s): @1=Qualunque oggetto appartenente al gruppo: @1
Any item belonging to the group(s): @1=Qualunque oggetto appartenente al/ai gruppo/i: @1
Any black dye=Qualunque tintura nera
Any black flower=Qualunque fiore nero
Any blue dye=Qualunque tintura blu
Any blue flower=Qualunque fiore blu
Any brown dye=Qualunque tintura marrone
Any coal=Qualunque carbone
Any cyan dye=Qualunque tintura ciano
Any dark green dye=Qualunque tintura verde scura
Any dark grey dye=Qualunque tintura grigio scura
Any green dye=Qualunque tintura verde
Any green flower=Qualunque fiore verde
Any grey dye=Qualunque tintura grigia
Any kind of stone block=Qualunque tipo di blocco di pietra
Any magenta dye=Qualunque tintura magenta
Any orange dye=Qualunque tintura arancione
Any orange flower=Qualunque fiore arancione
Any pink dye=Qualunque tintura rosa
Any red dye=Qualunque tintura rossa
Any red flower=Qualunque fiore rosso
Any sand=Qualunque sabbia
Any stick=Qualunque bastone
Any tree=Qualunque albero
Any vessel=Qualunque contenitore
Any violet dye=Qualunque tintura viola
Any violet flower=Qualunque fiore viola
Any white dye=Qualunque tintura bianca
Any white flower=Qualunque fiore bianco
Any wood planks=Qualunque asse di legno
Any wool=Qualunque lana
Any yellow dye=Qualunque tintura gialla
Any yellow flower=Qualunque fiore giallo
Recipe's too big to be displayed (@1x@2)=La ricetta è troppo grande per essere mostrata (@1x@2)
Shapeless=Senza forma
Cooking=Cottura
No item to show=Nessun oggetto da mostrare
Collect items to reveal more recipes=Raccogli oggetti per svelare più ricette
Show recipe(s) of the pointed node=Mostra la ricetta del nodo puntato
Show recipe(s) of the pointed node=Mostra la/le ricetta/e del nodo puntato
No node pointed=Nessun nodo puntato
You don't know a recipe for this node=Non conosci una ricetta per questo nodo
No recipe for this node=Nessuna ricetta per questo nodo
You don't know a recipe or usage for this item=Non conosci una ricetta o un utilizzo per questo oggetto
No recipe or usage for this item=Nessuna ricetta o utilizzo per questo oggetto
Digging=Scavando
Digging Chance=Probabilità di scavare
@1 of chance to drop=@1 di probabilità di rilascio

View File

@ -3,6 +3,7 @@
Craft Guide=
Crafting Guide=
Crafting Guide Sign=
Bookmarks=
Usage @1 of @2=
Recipe @1 of @2=
No recipes=
@ -14,6 +15,37 @@ Replaced by @1 on burning=
Replaced by @1 on crafting=
Repairable by step of @1=
Any item belonging to the group(s): @1=
Any black dye=
Any black flower=
Any blue dye=
Any blue flower=
Any brown dye=
Any coal=
Any cyan dye=
Any dark green dye=
Any dark grey dye=
Any green dye=
Any green flower=
Any grey dye=
Any kind of stone block=
Any magenta dye=
Any orange dye=
Any orange flower=
Any pink dye=
Any red dye=
Any red flower=
Any sand=
Any stick=
Any tree=
Any vessel=
Any violet dye=
Any violet flower=
Any white dye=
Any white flower=
Any wood planks=
Any wool=
Any yellow dye=
Any yellow flower=
Recipe's too big to be displayed (@1x@2)=
Shapeless=
Cooking=

View File

@ -1,5 +1,12 @@
# The progressive mode shows recipes you can craft from items you ever had in your inventory.
craftguide_progressive_mode (Progressive Mode) bool false
craftguide_progressive_mode (Learn crafting recipes progressively) bool false
# Integration in the default Minetest Game inventory.
craftguide_sfinv_only (Sfinv only) bool false
craftguide_sfinv_only (Crafting Guide in inventory only) bool false
# Enable pre-caching of item recipes.
# Do NOT disable the first time the mod loads.
# Disabling the auto-caching will result in faster mod loading.
# If you enable or disable mods, or edit the current mod recipes in your setup, you SHOULD re-enable this setting until the next caching at least.
# Usage at your own risk.
craftguide_autocache (Auto-caching of recipes) bool true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 B

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -246,7 +246,7 @@ if minetest.get_modpath("bucket") then
S("Dwarven Syrup Bucket")
)
if minetest.get_modpath("crafting") then
if minetest.get_modpath("simplecrafting_lib") then
simplecrafting_lib.register("furnace", {
input = {
["bucket:bucket_empty"] = 1,

View File

@ -53,14 +53,6 @@ mobs:register_mob('mobs_creatures:astronaut', {
on_rightclick = function(self, clicker)
mobs_trader(self, clicker, entity, mobs.lunar_human)
end,
on_spawn = function(self)
self.nametag = "Astronaut"
self.object:set_properties({
nametag = self.nametag,
nametag_color = "#FFFFFF"
})
return true -- return true so on_spawn is run once only
end,
})

View File

@ -1,4 +1,4 @@
-- TODO: Add drops.
-- TODO: Add drops, mese-based trading npc.
-- GREY CIVILIAN ALIEN
mobs:register_mob("mobs_creatures:grey_civilian", {
type = "animal",

View File

@ -1,3 +1,4 @@
-- edited this version to not have nametags on the HUD.
local S = mobs.intllib
@ -103,14 +104,6 @@ mobs:register_mob("mobs_npc:trader", {
on_rightclick = function(self, clicker)
mobs_trader(self, clicker, entity, mobs.human)
end,
on_spawn = function(self)
self.nametag = S("Trader")
self.object:set_properties({
nametag = self.nametag,
nametag_color = "#FFFFFF"
})
return true -- return true so on_spawn is run once only
end,
})
--This code comes almost exclusively from the trader and inventory of mobf, by Sapier.
@ -199,10 +192,6 @@ function mobs_trader(self, clicker, entity, race)
self.game_name = tostring(race.names[math.random(1, #race.names)])
self.nametag = S("Trader @1", self.game_name)
self.object:set_properties({
nametag = self.nametag,
nametag_color = "#00FF00"
})
end
if self.trades == nil then

View File

@ -1,6 +1,11 @@
# URGENT PRIORITY
#### URGENT PRIORITY ####
Exploit (Material duplication): Nether portal is exploitable for infinite portal-materials if a magic_mirror is used. Break exit portal, mirror home, then reinitiate portal entrance.
Mobs are flashing on hit. Need to look into the API to see why this is.
# HIGHER PRIORITY
#### HIGHER PRIORITY ####
# LOWER PRIORITY
#### LOWER PRIORITY ####
Add particles that go towards void chest.
### IDEAS ####
Implement stargate-like portal with nether portal_api?