MBMNET-App v2.8.2: Quellcode (GPLv3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# Build-Ergebnisse
|
||||||
|
build/
|
||||||
|
*.3dsx
|
||||||
|
*.cia
|
||||||
|
*.elf
|
||||||
|
*.smdh
|
||||||
|
*.o
|
||||||
|
*.bin
|
||||||
|
ftpmodul/build/
|
||||||
|
|
||||||
|
# Alter Versuch (nicht mehr in Benutzung)
|
||||||
|
unbenutzt-kontowechsel/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<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>.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#---------------------------------------------------------------------------------
|
||||||
|
# MBMNET Switcher - 3DS Homebrew (.3dsx)
|
||||||
|
# Benötigt devkitPro mit dem Paket "3ds-dev" (libctru, citro3d, citro2d).
|
||||||
|
#---------------------------------------------------------------------------------
|
||||||
|
.SUFFIXES:
|
||||||
|
|
||||||
|
ifeq ($(strip $(DEVKITARM)),)
|
||||||
|
$(error "DEVKITARM ist nicht gesetzt. Bitte devkitPro installieren.")
|
||||||
|
endif
|
||||||
|
|
||||||
|
TOPDIR ?= $(CURDIR)
|
||||||
|
include $(DEVKITARM)/3ds_rules
|
||||||
|
|
||||||
|
TARGET := mbmnet
|
||||||
|
BUILD := build
|
||||||
|
SOURCES := source
|
||||||
|
INCLUDES := source
|
||||||
|
|
||||||
|
# Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer
|
||||||
|
VERSION := 2.8.2
|
||||||
|
|
||||||
|
APP_TITLE := MBMNET
|
||||||
|
APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk (v$(VERSION))
|
||||||
|
APP_AUTHOR := MBMNET
|
||||||
|
ICON := icon.png
|
||||||
|
|
||||||
|
# CIA: feste Titel-ID 000400000FB4E000
|
||||||
|
APP_PRODUCT_CODE := CTR-H-MBMN
|
||||||
|
APP_UNIQUE_ID := 0xFB4E0
|
||||||
|
VERSION_PARTS := $(subst ., ,$(VERSION))
|
||||||
|
# Titelversion für die CIA. Muss bei jedem Release steigen, sonst verweigert der 3DS die
|
||||||
|
# Installation (Downgrade). makerom übernimmt die mittlere Stelle nicht, deshalb kodieren wir
|
||||||
|
# Haupt- und Unterversion zusammen in der ersten Stelle: 2.2.3 -> 22.0.3 (Unterversion max. 9).
|
||||||
|
TITLE_MAJOR := $(shell echo $$(( $(word 1,$(VERSION_PARTS)) * 10 + $(word 2,$(VERSION_PARTS)) )))
|
||||||
|
VERSION_NUM := $(shell echo $$(( ($(TITLE_MAJOR) << 10) | $(word 3,$(VERSION_PARTS)) )))
|
||||||
|
|
||||||
|
ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft
|
||||||
|
CFLAGS := -g -Wall -O2 -mword-relocations -ffunction-sections $(ARCH) $(INCLUDE) -D__3DS__ -DAPP_VERSION=\"$(VERSION)\"
|
||||||
|
ASFLAGS := -g $(ARCH)
|
||||||
|
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
|
||||||
|
LIBS := -lcitro2d -lcitro3d -lctru -lm
|
||||||
|
LIBDIRS := $(CTRULIB) $(PORTLIBS)
|
||||||
|
|
||||||
|
ifneq ($(BUILD),$(notdir $(CURDIR)))
|
||||||
|
|
||||||
|
export OUTPUT := $(CURDIR)/$(TARGET)
|
||||||
|
export TOPDIR := $(CURDIR)
|
||||||
|
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir))
|
||||||
|
export DEPSDIR := $(CURDIR)/$(BUILD)
|
||||||
|
|
||||||
|
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
|
||||||
|
export LD := $(CC)
|
||||||
|
export OFILES := $(CFILES:.c=.o)
|
||||||
|
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
|
||||||
|
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
|
||||||
|
-I$(CURDIR)/$(BUILD)
|
||||||
|
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
|
||||||
|
export APP_ICON := $(TOPDIR)/$(ICON)
|
||||||
|
export _3DSXDEPS := $(OUTPUT).smdh
|
||||||
|
export _3DSXFLAGS += --smdh=$(OUTPUT).smdh
|
||||||
|
|
||||||
|
.PHONY: all clean cia $(BUILD)
|
||||||
|
|
||||||
|
all: $(BUILD)
|
||||||
|
|
||||||
|
# CIA zum Installieren mit FBI o. Ä. (braucht makerom und bannertool, siehe tools/Dockerfile)
|
||||||
|
cia: $(BUILD)
|
||||||
|
@bannertool makebanner -i assets/banner.png -a assets/banner.wav -o $(BUILD)/banner.bnr
|
||||||
|
@bannertool makesmdh -s "$(APP_TITLE)" -l "$(APP_DESCRIPTION)" -p "$(APP_AUTHOR)" -i $(ICON) -o $(BUILD)/icon.icn
|
||||||
|
@makerom -f cia -o $(TARGET).cia -elf $(TARGET).elf -rsf cia.rsf -target t -exefslogo \
|
||||||
|
-icon $(BUILD)/icon.icn -banner $(BUILD)/banner.bnr \
|
||||||
|
-DAPP_TITLE="$(APP_TITLE)" -DAPP_PRODUCT_CODE="$(APP_PRODUCT_CODE)" -DAPP_UNIQUE_ID="$(APP_UNIQUE_ID)" \
|
||||||
|
-ver $(VERSION_NUM)
|
||||||
|
@echo built ... $(TARGET).cia
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
@mkdir -p $@
|
||||||
|
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
|
||||||
|
|
||||||
|
# FTP-Hintergrund-Modul bauen und als Byte-Feld in die App einbetten
|
||||||
|
ftpmodul/ftpmodul.cxi: $(wildcard ftpmodul/source/*) ftpmodul/ftpmodul.rsf ftpmodul/Makefile
|
||||||
|
@$(MAKE) --no-print-directory -C ftpmodul
|
||||||
|
|
||||||
|
source/ftpmodul_data.c: ftpmodul/ftpmodul.cxi
|
||||||
|
@echo "// Automatisch erzeugt aus ftpmodul/ftpmodul.cxi - nicht bearbeiten" > $@
|
||||||
|
@echo "#include <3ds/types.h>" >> $@
|
||||||
|
@echo "const u8 ftpmodul_cxi[] __attribute__((aligned(4))) = {" >> $@
|
||||||
|
@od -An -v -tx1 $< | sed -e 's/ \([0-9a-f][0-9a-f]\)/0x\1,/g' >> $@
|
||||||
|
@echo "};" >> $@
|
||||||
|
@echo "const u32 ftpmodul_cxi_size = sizeof(ftpmodul_cxi);" >> $@
|
||||||
|
@echo "const u32 ftpmodul_status_addr = $$(cat ftpmodul/ftpmodul.sym);" >> $@
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@echo clean ...
|
||||||
|
@rm -fr $(BUILD) $(TARGET).3dsx $(TARGET).smdh $(TARGET).elf $(TARGET).cia source/ftpmodul_data.c
|
||||||
|
@$(MAKE) --no-print-directory -C ftpmodul clean
|
||||||
|
|
||||||
|
else
|
||||||
|
|
||||||
|
DEPENDS := $(OFILES:.o=.d)
|
||||||
|
|
||||||
|
$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS)
|
||||||
|
$(OUTPUT).elf : $(OFILES)
|
||||||
|
|
||||||
|
-include $(DEPENDS)
|
||||||
|
|
||||||
|
endif
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# MBMNET-App (Quellcode)
|
||||||
|
|
||||||
|
Die 3DS-App von MBMNET: schaltet den 3DS zwischen MBMNET und Nintendo um, legt ein eigenes
|
||||||
|
MBMNET-Freundekonto an, aktiviert Miiverse und SpotPass über MBMNET und aktualisiert sich selbst.
|
||||||
|
|
||||||
|
Fertige Versionen (`.cia` / `.3dsx`) und Anleitung: [MrMaho/MBMNET](https://git.mbm.wtf/MrMaho/MBMNET)
|
||||||
|
|
||||||
|
## Bauen
|
||||||
|
|
||||||
|
Am einfachsten mit Docker (enthält devkitARM, libctru, makerom und bannertool):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t mbmnet-3ds-build tools
|
||||||
|
docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c 'make && make cia'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis: `mbmnet.3dsx` und `mbmnet.cia`. Version steht in `Makefile` (`VERSION`).
|
||||||
|
|
||||||
|
`release.sh` baut, legt die Dateien ins Repo `MBMNET` und erstellt dort ein Gitea-Release.
|
||||||
|
|
||||||
|
## Aufbau
|
||||||
|
|
||||||
|
| Datei | Was |
|
||||||
|
|---|---|
|
||||||
|
| `source/main.c` | Oberfläche, Umschalten MBMNET / Nintendo, Werkzeuge |
|
||||||
|
| `source/patcher.c` | Luma3DS-Patches (`/luma/sysmodules`, `/luma/titles`) schreiben und entfernen |
|
||||||
|
| `source/olvpatch.c` | Miiverse-Patch (Discovery-Adresse und MBMNET-Zertifikat) |
|
||||||
|
| `source/frdswitch.c` | Eigenes MBMNET-Freundekonto |
|
||||||
|
| `source/nnid.c` | Verknüpfte Nintendo Network ID lösen |
|
||||||
|
| `source/spotpass.c` | SpotPass-Aufgabe für MBMNET-Mitteilungen |
|
||||||
|
| `source/lumaconfig.c` | Spiel-Patches in der Luma-Konfiguration einschalten |
|
||||||
|
| `source/update.c` | Selbst-Update aus dem Repo `MBMNET` |
|
||||||
|
| `source/admin.c` | Entwickler-Werkzeuge nur für Admins |
|
||||||
|
| `ftpmodul/` | Kleiner FTP-Server für die Entwickler-Werkzeuge |
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
GPLv3, siehe `LICENSE`. Die Miiverse-Patches stammen aus Pretendos
|
||||||
|
[Nimbus](https://github.com/PretendoNetwork/Nimbus) (GPLv3); das eigene Freundekonto ist nach Nimbus' Vorbild gebaut.
|
||||||
|
|
||||||
|
Große Teile des Codes wurden mit Hilfe eines KI-Assistenten (Claude) geschrieben.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,272 @@
|
|||||||
|
# Einstellungen für die CIA-Version der MBMNET-App (makerom).
|
||||||
|
# Werte mit $(...) setzt das Makefile.
|
||||||
|
|
||||||
|
BasicInfo:
|
||||||
|
Title : $(APP_TITLE)
|
||||||
|
ProductCode : $(APP_PRODUCT_CODE)
|
||||||
|
Logo : Homebrew
|
||||||
|
|
||||||
|
TitleInfo:
|
||||||
|
Category : Application
|
||||||
|
UniqueId : $(APP_UNIQUE_ID)
|
||||||
|
|
||||||
|
Option:
|
||||||
|
UseOnSD : true
|
||||||
|
FreeProductCode : true
|
||||||
|
MediaFootPadding : false
|
||||||
|
EnableCrypt : false
|
||||||
|
EnableCompress : true
|
||||||
|
|
||||||
|
AccessControlInfo:
|
||||||
|
CoreVersion : 2
|
||||||
|
DescVersion : 2
|
||||||
|
ReleaseKernelMajor : "02"
|
||||||
|
ReleaseKernelMinor : "33"
|
||||||
|
|
||||||
|
# Eigene Extdata (ID = Unique ID): Speicher, den SpotPass (BOSS) für die Mitteilungs-Aufgabe braucht
|
||||||
|
UseExtSaveData : true
|
||||||
|
ExtSaveDataId : 0xFB4E0
|
||||||
|
|
||||||
|
# Dateisystem: SD-Karte (Patches, Updates) und Systemtitel lesen (Module patchen)
|
||||||
|
FileSystemAccess:
|
||||||
|
- CategorySystemApplication
|
||||||
|
- CategoryHardwareCheck
|
||||||
|
- CategoryFileSystemTool
|
||||||
|
- Debug
|
||||||
|
- TwlCardBackup
|
||||||
|
- TwlNandData
|
||||||
|
- Boss
|
||||||
|
- DirectSdmc
|
||||||
|
- Core
|
||||||
|
- CtrNandRo
|
||||||
|
- CtrNandRw
|
||||||
|
- CtrNandRoWrite
|
||||||
|
- CategorySystemSettings
|
||||||
|
- CardBoard
|
||||||
|
- ExportImportIvs
|
||||||
|
- DirectSdmcWrite
|
||||||
|
- SwitchCleanup
|
||||||
|
- SaveDataMove
|
||||||
|
- Shop
|
||||||
|
- Shell
|
||||||
|
- CategoryHomeMenu
|
||||||
|
- SeedDB
|
||||||
|
IoAccessControl:
|
||||||
|
- FsMountNand
|
||||||
|
- FsMountNandRoWrite
|
||||||
|
- FsMountTwln
|
||||||
|
- FsMountWnand
|
||||||
|
- FsMountCardSpi
|
||||||
|
- UseSdif3
|
||||||
|
- CreateSeed
|
||||||
|
- UseCardSpi
|
||||||
|
|
||||||
|
MemoryType : Application
|
||||||
|
SystemMode : 64MB
|
||||||
|
IdealProcessor : 0
|
||||||
|
AffinityMask : 1
|
||||||
|
Priority : 16
|
||||||
|
MaxCpu : 0x9E
|
||||||
|
HandleTableSize : 0x200
|
||||||
|
DisableDebug : false
|
||||||
|
EnableForceDebug : false
|
||||||
|
CanWriteSharedPage : true
|
||||||
|
CanUsePrivilegedPriority : false
|
||||||
|
CanUseNonAlphabetAndNumber : true
|
||||||
|
PermitMainFunctionArgument : true
|
||||||
|
CanShareDeviceMemory : true
|
||||||
|
RunnableOnSleep : false
|
||||||
|
SpecialMemoryArrange : true
|
||||||
|
|
||||||
|
SystemModeExt : Legacy
|
||||||
|
CpuSpeed : 804MHz
|
||||||
|
EnableL2Cache : true
|
||||||
|
CanAccessCore2 : true
|
||||||
|
|
||||||
|
IORegisterMapping:
|
||||||
|
- 1ff00000-1ff7ffff
|
||||||
|
MemoryMapping:
|
||||||
|
- 1f000000-1f5fffff:r
|
||||||
|
|
||||||
|
SystemCallAccess:
|
||||||
|
ControlMemory: 1
|
||||||
|
QueryMemory: 2
|
||||||
|
ExitProcess: 3
|
||||||
|
GetProcessAffinityMask: 4
|
||||||
|
SetProcessAffinityMask: 5
|
||||||
|
GetProcessIdealProcessor: 6
|
||||||
|
SetProcessIdealProcessor: 7
|
||||||
|
CreateThread: 8
|
||||||
|
ExitThread: 9
|
||||||
|
SleepThread: 10
|
||||||
|
GetThreadPriority: 11
|
||||||
|
SetThreadPriority: 12
|
||||||
|
GetThreadAffinityMask: 13
|
||||||
|
SetThreadAffinityMask: 14
|
||||||
|
GetThreadIdealProcessor: 15
|
||||||
|
SetThreadIdealProcessor: 16
|
||||||
|
GetCurrentProcessorNumber: 17
|
||||||
|
Run: 18
|
||||||
|
CreateMutex: 19
|
||||||
|
ReleaseMutex: 20
|
||||||
|
CreateSemaphore: 21
|
||||||
|
ReleaseSemaphore: 22
|
||||||
|
CreateEvent: 23
|
||||||
|
SignalEvent: 24
|
||||||
|
ClearEvent: 25
|
||||||
|
CreateTimer: 26
|
||||||
|
SetTimer: 27
|
||||||
|
CancelTimer: 28
|
||||||
|
ClearTimer: 29
|
||||||
|
CreateMemoryBlock: 30
|
||||||
|
MapMemoryBlock: 31
|
||||||
|
UnmapMemoryBlock: 32
|
||||||
|
CreateAddressArbiter: 33
|
||||||
|
ArbitrateAddress: 34
|
||||||
|
CloseHandle: 35
|
||||||
|
WaitSynchronization1: 36
|
||||||
|
WaitSynchronizationN: 37
|
||||||
|
SignalAndWait: 38
|
||||||
|
DuplicateHandle: 39
|
||||||
|
GetSystemTick: 40
|
||||||
|
GetHandleInfo: 41
|
||||||
|
GetSystemInfo: 42
|
||||||
|
GetProcessInfo: 43
|
||||||
|
GetThreadInfo: 44
|
||||||
|
ConnectToPort: 45
|
||||||
|
SendSyncRequest1: 46
|
||||||
|
SendSyncRequest2: 47
|
||||||
|
SendSyncRequest3: 48
|
||||||
|
SendSyncRequest4: 49
|
||||||
|
SendSyncRequest: 50
|
||||||
|
OpenProcess: 51
|
||||||
|
OpenThread: 52
|
||||||
|
GetProcessId: 53
|
||||||
|
GetProcessIdOfThread: 54
|
||||||
|
GetThreadId: 55
|
||||||
|
GetResourceLimit: 56
|
||||||
|
GetResourceLimitLimitValues: 57
|
||||||
|
GetResourceLimitCurrentValues: 58
|
||||||
|
GetThreadContext: 59
|
||||||
|
Break: 60
|
||||||
|
OutputDebugString: 61
|
||||||
|
ControlPerformanceCounter: 62
|
||||||
|
CreatePort: 71
|
||||||
|
CreateSessionToPort: 72
|
||||||
|
CreateSession: 73
|
||||||
|
AcceptSession: 74
|
||||||
|
ReplyAndReceive1: 75
|
||||||
|
ReplyAndReceive2: 76
|
||||||
|
ReplyAndReceive3: 77
|
||||||
|
ReplyAndReceive4: 78
|
||||||
|
ReplyAndReceive: 79
|
||||||
|
BindInterrupt: 80
|
||||||
|
UnbindInterrupt: 81
|
||||||
|
InvalidateProcessDataCache: 82
|
||||||
|
StoreProcessDataCache: 83
|
||||||
|
FlushProcessDataCache: 84
|
||||||
|
StartInterProcessDma: 85
|
||||||
|
StopDma: 86
|
||||||
|
GetDmaState: 87
|
||||||
|
RestartDma: 88
|
||||||
|
DebugActiveProcess: 96
|
||||||
|
BreakDebugProcess: 97
|
||||||
|
TerminateDebugProcess: 98
|
||||||
|
GetProcessDebugEvent: 99
|
||||||
|
ContinueDebugEvent: 100
|
||||||
|
GetProcessList: 101
|
||||||
|
GetThreadList: 102
|
||||||
|
GetDebugThreadContext: 103
|
||||||
|
SetDebugThreadContext: 104
|
||||||
|
QueryDebugProcessMemory: 105
|
||||||
|
ReadProcessMemory: 106
|
||||||
|
WriteProcessMemory: 107
|
||||||
|
SetHardwareBreakPoint: 108
|
||||||
|
GetDebugThreadParam: 109
|
||||||
|
ControlProcessMemory: 112
|
||||||
|
MapProcessMemory: 113
|
||||||
|
UnmapProcessMemory: 114
|
||||||
|
CreateCodeSet: 115
|
||||||
|
CreateProcess: 117
|
||||||
|
TerminateProcess: 118
|
||||||
|
SetProcessResourceLimits: 119
|
||||||
|
CreateResourceLimit: 120
|
||||||
|
SetResourceLimitValues: 121
|
||||||
|
AddCodeSegment: 122
|
||||||
|
Backdoor: 123
|
||||||
|
KernelSetState: 124
|
||||||
|
QueryProcessMemory: 125
|
||||||
|
|
||||||
|
# Dienste: u. a. am:net (Updates installieren), http:C (Updates laden), fs:USER, ptm:sysm (Neustart),
|
||||||
|
# frd:a (Freundekonto umschalten), pm:app (FTP-Modul starten/beenden)
|
||||||
|
ServiceAccessControl:
|
||||||
|
- APT:U
|
||||||
|
- ac:u
|
||||||
|
- act:a
|
||||||
|
- act:u
|
||||||
|
- am:net
|
||||||
|
- boss:U
|
||||||
|
- boss:P
|
||||||
|
- cecd:u
|
||||||
|
- cfg:nor
|
||||||
|
- cfg:u
|
||||||
|
- csnd:SND
|
||||||
|
- dsp::DSP
|
||||||
|
- frd:a
|
||||||
|
- frd:u
|
||||||
|
- fs:USER
|
||||||
|
- gsp::Gpu
|
||||||
|
- gsp::Lcd
|
||||||
|
- hid:USER
|
||||||
|
- http:C
|
||||||
|
- ir:rst
|
||||||
|
- ir:u
|
||||||
|
- ir:USER
|
||||||
|
- mic:u
|
||||||
|
- ndm:u
|
||||||
|
- news:s
|
||||||
|
- nwm::EXT
|
||||||
|
- nwm::UDS
|
||||||
|
- pm:app
|
||||||
|
- ptm:sysm
|
||||||
|
- ptm:u
|
||||||
|
- pxi:dev
|
||||||
|
- soc:U
|
||||||
|
- ssl:C
|
||||||
|
- y2r:u
|
||||||
|
|
||||||
|
SystemControlInfo:
|
||||||
|
SaveDataSize: 0KB
|
||||||
|
RemasterVersion: 0
|
||||||
|
StackSize: 0x40000
|
||||||
|
Dependency:
|
||||||
|
ac: 0x0004013000002402
|
||||||
|
am: 0x0004013000001502
|
||||||
|
boss: 0x0004013000003402
|
||||||
|
camera: 0x0004013000001602
|
||||||
|
cecd: 0x0004013000002602
|
||||||
|
cfg: 0x0004013000001702
|
||||||
|
codec: 0x0004013000001802
|
||||||
|
csnd: 0x0004013000002702
|
||||||
|
dlp: 0x0004013000002802
|
||||||
|
dsp: 0x0004013000001a02
|
||||||
|
friends: 0x0004013000003202
|
||||||
|
gpio: 0x0004013000001b02
|
||||||
|
gsp: 0x0004013000001c02
|
||||||
|
hid: 0x0004013000001d02
|
||||||
|
http: 0x0004013000002902
|
||||||
|
i2c: 0x0004013000001e02
|
||||||
|
ir: 0x0004013000003302
|
||||||
|
mcu: 0x0004013000001f02
|
||||||
|
mic: 0x0004013000002002
|
||||||
|
ndm: 0x0004013000002b02
|
||||||
|
news: 0x0004013000003502
|
||||||
|
nim: 0x0004013000002c02
|
||||||
|
nwm: 0x0004013000002d02
|
||||||
|
pdn: 0x0004013000002102
|
||||||
|
ps: 0x0004013000003102
|
||||||
|
ptm: 0x0004013000002202
|
||||||
|
ro: 0x0004013000003702
|
||||||
|
socket: 0x0004013000002e02
|
||||||
|
spi: 0x0004013000002302
|
||||||
|
ssl: 0x0004013000002f02
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# MBMNET-FTP-Modul (Hintergrund-Prozess). Wird vom Makefile der App mitgebaut und in die App eingebettet.
|
||||||
|
ifeq ($(strip $(DEVKITARM)),)
|
||||||
|
$(error "DEVKITARM ist nicht gesetzt.")
|
||||||
|
endif
|
||||||
|
|
||||||
|
CC := $(DEVKITARM)/bin/arm-none-eabi-gcc
|
||||||
|
ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft
|
||||||
|
CFLAGS := -O2 -Wall -mword-relocations -ffunction-sections $(ARCH) -D__3DS__ -I$(DEVKITPRO)/libctru/include
|
||||||
|
LDFLAGS := -specs=3dsx.specs $(ARCH) -Wl,--gc-sections -L$(DEVKITPRO)/libctru/lib
|
||||||
|
SOURCES := $(wildcard source/*.c)
|
||||||
|
|
||||||
|
ftpmodul.cxi: ftpmodul.elf ftpmodul.rsf
|
||||||
|
@makerom -f ncch -rsf ftpmodul.rsf -elf ftpmodul.elf -o ftpmodul.cxi
|
||||||
|
@# Lumas pm stürzt ab, wenn die Kennung eines Systemmoduls nicht auf 02 endet (panic 4)
|
||||||
|
@test "$$(od -An -tx1 -j 0x118 -N 1 ftpmodul.cxi | tr -d ' ')" = "02" || { echo "FEHLER: Kennung endet nicht auf 02"; rm -f ftpmodul.cxi; exit 1; }
|
||||||
|
@echo built ... ftpmodul.cxi
|
||||||
|
|
||||||
|
ftpmodul.elf: $(SOURCES) $(wildcard source/*.h)
|
||||||
|
@$(CC) $(CFLAGS) $(SOURCES) $(LDFLAGS) -lctru -lm -o $@
|
||||||
|
@# Adresse des Statusblocks für die App (liest ihn per Debug-Zugriff)
|
||||||
|
@$(DEVKITARM)/bin/arm-none-eabi-nm $@ | awk '$$3 == "ftpStatus" { print "0x" $$1 }' > ftpmodul.sym
|
||||||
|
@test -s ftpmodul.sym || { echo "FEHLER: ftpStatus nicht gefunden"; exit 1; }
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -f ftpmodul.elf ftpmodul.cxi ftpmodul.sym
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# MBMNET-FTP-Modul: Hintergrund-Prozess (Systemmodul-Kennung 0004013000F1E002).
|
||||||
|
# Luma lädt es als /luma/sysmodules/0004013000F1E002.cxi, die MBMNET-App startet es.
|
||||||
|
BasicInfo:
|
||||||
|
Title : mbmnetftp
|
||||||
|
CompanyCode : "00"
|
||||||
|
ProductCode : CTR-S-MFTP
|
||||||
|
ContentType : Application
|
||||||
|
Logo : None
|
||||||
|
|
||||||
|
TitleInfo:
|
||||||
|
UniqueId : 0xF1E0
|
||||||
|
Category : Base
|
||||||
|
Version : 2 # = letzte Stelle der Kennung (02 = normales System). Muss 2 sein, sonst stürzt Lumas pm ab!
|
||||||
|
|
||||||
|
Option:
|
||||||
|
UseOnSD : false
|
||||||
|
FreeProductCode : true
|
||||||
|
MediaFootPadding : false
|
||||||
|
EnableCrypt : false
|
||||||
|
EnableCompress : false
|
||||||
|
|
||||||
|
AccessControlInfo:
|
||||||
|
IdealProcessor : 1
|
||||||
|
AffinityMask : 3
|
||||||
|
Priority : 40
|
||||||
|
DisableDebug : false
|
||||||
|
EnableForceDebug : false
|
||||||
|
CanWriteSharedPage : false
|
||||||
|
CanUsePrivilegedPriority : false
|
||||||
|
CanUseNonAlphabetAndNumber : false
|
||||||
|
PermitMainFunctionArgument : false
|
||||||
|
CanShareDeviceMemory : true
|
||||||
|
RunnableOnSleep : false
|
||||||
|
SpecialMemoryArrange : true
|
||||||
|
ResourceLimitCategory : Other
|
||||||
|
CoreVersion : 2
|
||||||
|
DescVersion : 2
|
||||||
|
MemoryType : Base
|
||||||
|
HandleTableSize : 0x80
|
||||||
|
|
||||||
|
SystemCallAccess:
|
||||||
|
ControlMemory: 1
|
||||||
|
QueryMemory: 2
|
||||||
|
ExitProcess: 3
|
||||||
|
GetProcessAffinityMask: 4
|
||||||
|
SetProcessAffinityMask: 5
|
||||||
|
GetProcessIdealProcessor: 6
|
||||||
|
SetProcessIdealProcessor: 7
|
||||||
|
CreateThread: 8
|
||||||
|
ExitThread: 9
|
||||||
|
SleepThread: 10
|
||||||
|
GetThreadPriority: 11
|
||||||
|
SetThreadPriority: 12
|
||||||
|
GetThreadAffinityMask: 13
|
||||||
|
SetThreadAffinityMask: 14
|
||||||
|
GetThreadIdealProcessor: 15
|
||||||
|
SetThreadIdealProcessor: 16
|
||||||
|
GetCurrentProcessorNumber: 17
|
||||||
|
Run: 18
|
||||||
|
CreateMutex: 19
|
||||||
|
ReleaseMutex: 20
|
||||||
|
CreateSemaphore: 21
|
||||||
|
ReleaseSemaphore: 22
|
||||||
|
CreateEvent: 23
|
||||||
|
SignalEvent: 24
|
||||||
|
ClearEvent: 25
|
||||||
|
CreateTimer: 26
|
||||||
|
SetTimer: 27
|
||||||
|
CancelTimer: 28
|
||||||
|
ClearTimer: 29
|
||||||
|
CreateMemoryBlock: 30
|
||||||
|
MapMemoryBlock: 31
|
||||||
|
UnmapMemoryBlock: 32
|
||||||
|
CreateAddressArbiter: 33
|
||||||
|
ArbitrateAddress: 34
|
||||||
|
CloseHandle: 35
|
||||||
|
WaitSynchronization1: 36
|
||||||
|
WaitSynchronizationN: 37
|
||||||
|
SignalAndWait: 38
|
||||||
|
DuplicateHandle: 39
|
||||||
|
GetSystemTick: 40
|
||||||
|
GetHandleInfo: 41
|
||||||
|
GetSystemInfo: 42
|
||||||
|
GetProcessInfo: 43
|
||||||
|
GetThreadInfo: 44
|
||||||
|
ConnectToPort: 45
|
||||||
|
SendSyncRequest1: 46
|
||||||
|
SendSyncRequest2: 47
|
||||||
|
SendSyncRequest3: 48
|
||||||
|
SendSyncRequest4: 49
|
||||||
|
SendSyncRequest: 50
|
||||||
|
OpenProcess: 51
|
||||||
|
OpenThread: 52
|
||||||
|
GetProcessId: 53
|
||||||
|
GetProcessIdOfThread: 54
|
||||||
|
GetThreadId: 55
|
||||||
|
GetResourceLimit: 56
|
||||||
|
GetResourceLimitLimitValues: 57
|
||||||
|
GetResourceLimitCurrentValues: 58
|
||||||
|
GetThreadContext: 59
|
||||||
|
Break: 60
|
||||||
|
OutputDebugString: 61
|
||||||
|
ControlPerformanceCounter: 62
|
||||||
|
CreatePort: 71
|
||||||
|
CreateSessionToPort: 72
|
||||||
|
CreateSession: 73
|
||||||
|
AcceptSession: 74
|
||||||
|
ReplyAndReceive1: 75
|
||||||
|
ReplyAndReceive2: 76
|
||||||
|
ReplyAndReceive3: 77
|
||||||
|
ReplyAndReceive4: 78
|
||||||
|
ReplyAndReceive: 79
|
||||||
|
BindInterrupt: 80
|
||||||
|
UnbindInterrupt: 81
|
||||||
|
InvalidateProcessDataCache: 82
|
||||||
|
StoreProcessDataCache: 83
|
||||||
|
FlushProcessDataCache: 84
|
||||||
|
StartInterProcessDma: 85
|
||||||
|
StopDma: 86
|
||||||
|
GetDmaState: 87
|
||||||
|
RestartDma: 88
|
||||||
|
DebugActiveProcess: 96
|
||||||
|
BreakDebugProcess: 97
|
||||||
|
TerminateDebugProcess: 98
|
||||||
|
GetProcessDebugEvent: 99
|
||||||
|
ContinueDebugEvent: 100
|
||||||
|
GetProcessList: 101
|
||||||
|
GetThreadList: 102
|
||||||
|
GetDebugThreadContext: 103
|
||||||
|
SetDebugThreadContext: 104
|
||||||
|
QueryDebugProcessMemory: 105
|
||||||
|
ReadProcessMemory: 106
|
||||||
|
WriteProcessMemory: 107
|
||||||
|
SetHardwareBreakPoint: 108
|
||||||
|
GetDebugThreadParam: 109
|
||||||
|
ControlProcessMemory: 112
|
||||||
|
MapProcessMemory: 113
|
||||||
|
UnmapProcessMemory: 114
|
||||||
|
CreateCodeSet: 115
|
||||||
|
CreateProcess: 117
|
||||||
|
TerminateProcess: 118
|
||||||
|
SetProcessResourceLimits: 119
|
||||||
|
CreateResourceLimit: 120
|
||||||
|
SetResourceLimitValues: 121
|
||||||
|
AddCodeSegment: 122
|
||||||
|
Backdoor: 123
|
||||||
|
KernelSetState: 124
|
||||||
|
QueryProcessMemory: 125
|
||||||
|
|
||||||
|
ServiceAccessControl:
|
||||||
|
- fs:USER
|
||||||
|
- soc:U
|
||||||
|
- ac:u
|
||||||
|
|
||||||
|
FileSystemAccess:
|
||||||
|
- DirectSdmc
|
||||||
|
- DirectSdmcWrite
|
||||||
|
|
||||||
|
SystemControlInfo:
|
||||||
|
SaveDataSize: 0KB
|
||||||
|
RemasterVersion: 0
|
||||||
|
StackSize: 0x8000
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
// FTP-Kern des MBMNET-FTP-Moduls (Hintergrund-Prozess, siehe main.c).
|
||||||
|
//
|
||||||
|
// - Ohne Benutzername/Passwort (jede Anmeldung wird angenommen), nur Passiv-Modus (PASV/EPSV).
|
||||||
|
// - Lesen: ganze SD-Karte. Schreiben/Löschen/Ordner anlegen: nur unter /3ds/mbmnet, damit niemand im
|
||||||
|
// WLAN Luma oder boot.firm überschreiben kann.
|
||||||
|
// - Eine Verbindung nach der anderen.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "ftp.h"
|
||||||
|
|
||||||
|
#define WRITE_ROOT "/3ds/mbmnet"
|
||||||
|
#define XFER_SIZE (32 * 1024)
|
||||||
|
|
||||||
|
|
||||||
|
// --- Hilfen ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void reply(int fd, const char *fmt, ...)
|
||||||
|
{
|
||||||
|
char line[512];
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
int n = vsnprintf(line, sizeof(line) - 2, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
if (n < 0) return;
|
||||||
|
if (n > (int)sizeof(line) - 3) n = sizeof(line) - 3;
|
||||||
|
line[n++] = '\r';
|
||||||
|
line[n++] = '\n';
|
||||||
|
send(fd, line, n, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_all(int fd, const char *buf, size_t len)
|
||||||
|
{
|
||||||
|
while (len) {
|
||||||
|
ssize_t n = send(fd, buf, len, 0);
|
||||||
|
if (n <= 0) return false;
|
||||||
|
buf += n;
|
||||||
|
len -= n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pfad auflösen: absolut oder relativ zu cwd, ".." und "." entfernen. Ergebnis beginnt mit "/".
|
||||||
|
static void resolve(const char *cwd, const char *arg, char *out, size_t len)
|
||||||
|
{
|
||||||
|
char tmp[512];
|
||||||
|
if (!arg || !arg[0]) snprintf(tmp, sizeof(tmp), "%s", cwd);
|
||||||
|
else if (arg[0] == '/') snprintf(tmp, sizeof(tmp), "%s", arg);
|
||||||
|
else snprintf(tmp, sizeof(tmp), "%s/%s", strcmp(cwd, "/") ? cwd : "", arg);
|
||||||
|
|
||||||
|
char *parts[64];
|
||||||
|
int count = 0;
|
||||||
|
for (char *p = strtok(tmp, "/"); p && count < 64; p = strtok(NULL, "/")) {
|
||||||
|
if (!strcmp(p, ".")) continue;
|
||||||
|
if (!strcmp(p, "..")) { if (count) count--; continue; }
|
||||||
|
parts[count++] = p;
|
||||||
|
}
|
||||||
|
size_t o = 0;
|
||||||
|
out[0] = 0;
|
||||||
|
for (int i = 0; i < count && o + 2 < len; i++) o += snprintf(out + o, len - o, "/%s", parts[i]);
|
||||||
|
if (!count) snprintf(out, len, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sd_path(const char *path, char *out, size_t len) { snprintf(out, len, "sdmc:%s", path); }
|
||||||
|
|
||||||
|
static bool writable(const char *path)
|
||||||
|
{
|
||||||
|
size_t n = strlen(WRITE_ROOT);
|
||||||
|
return !strncmp(path, WRITE_ROOT, n) && (path[n] == 0 || path[n] == '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Daten-Verbindung (passiv) ----------------------------------------------------------
|
||||||
|
|
||||||
|
static int open_passive(int ctrl, bool extended)
|
||||||
|
{
|
||||||
|
static int nextPort = CONTROL_PORT + 1;
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY };
|
||||||
|
for (int tries = 0; tries < 50; tries++) {
|
||||||
|
int port = nextPort++;
|
||||||
|
if (nextPort > CONTROL_PORT + 200) nextPort = CONTROL_PORT + 1;
|
||||||
|
addr.sin_port = htons(port);
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0 && listen(fd, 1) == 0) {
|
||||||
|
u32 ip = (u32)gethostid();
|
||||||
|
if (extended) reply(ctrl, "229 Entering Extended Passive Mode (|||%d|)", port);
|
||||||
|
else reply(ctrl, "227 Entering Passive Mode (%lu,%lu,%lu,%lu,%d,%d)", (unsigned long)(ip & 0xFF),
|
||||||
|
(unsigned long)((ip >> 8) & 0xFF), (unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24),
|
||||||
|
port >> 8, port & 0xFF);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wartet auf den Client (höchstens 10 s) und gibt die Daten-Verbindung zurück
|
||||||
|
static int accept_data(int pasvFd)
|
||||||
|
{
|
||||||
|
struct pollfd p = { .fd = pasvFd, .events = POLLIN };
|
||||||
|
if (poll(&p, 1, 10000) <= 0) return -1;
|
||||||
|
return accept(pasvFd, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Befehle -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Ein Eintrag im MLSD/MLST-Format (maschinenlesbar, wie FileZilla es abfragt)
|
||||||
|
static int mlsx_fact(char *out, size_t len, const struct stat *st, bool dir, const char *name)
|
||||||
|
{
|
||||||
|
char modify[20] = "20000101000000";
|
||||||
|
if (st->st_mtime > 0) strftime(modify, sizeof(modify), "%Y%m%d%H%M%S", gmtime(&st->st_mtime));
|
||||||
|
return snprintf(out, len, "type=%s;size=%llu;modify=%s;perm=%s; %s\r\n", dir ? "dir" : "file",
|
||||||
|
(unsigned long long)(dir ? 0 : st->st_size), modify, dir ? "el" : "r", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum { LIST_UNIX, LIST_NAMES, LIST_MLSD };
|
||||||
|
|
||||||
|
static void list_dir(int ctrl, int data, const char *path, int mode)
|
||||||
|
{
|
||||||
|
char sd[600];
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
DIR *d = opendir(sd);
|
||||||
|
if (!d) { reply(ctrl, "550 Ordner nicht gefunden"); return; }
|
||||||
|
reply(ctrl, "150 Liste folgt");
|
||||||
|
struct dirent *e;
|
||||||
|
char line[700], child[900];
|
||||||
|
while ((e = readdir(d))) {
|
||||||
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||||
|
if (mode == LIST_NAMES) { int n = snprintf(line, sizeof(line), "%s\r\n", e->d_name); send_all(data, line, n); continue; }
|
||||||
|
snprintf(child, sizeof(child), "%s/%s", sd, e->d_name);
|
||||||
|
struct stat st = { 0 };
|
||||||
|
stat(child, &st);
|
||||||
|
bool dir = S_ISDIR(st.st_mode) || e->d_type == DT_DIR;
|
||||||
|
if (mode == LIST_MLSD) {
|
||||||
|
int n = mlsx_fact(line, sizeof(line), &st, dir, e->d_name);
|
||||||
|
if (!send_all(data, line, n)) break;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char date[16] = "Jan 1 2000";
|
||||||
|
if (st.st_mtime > 0) strftime(date, sizeof(date), "%b %d %H:%M", localtime(&st.st_mtime));
|
||||||
|
int n = snprintf(line, sizeof(line), "%s 1 3ds 3ds %10llu %s %s\r\n", dir ? "drwxr-xr-x" : "-rw-r--r--",
|
||||||
|
(unsigned long long)(dir ? 0 : st.st_size), date, e->d_name);
|
||||||
|
if (!send_all(data, line, n)) break;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
reply(ctrl, "226 Fertig");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void retrieve(int ctrl, int data, const char *path)
|
||||||
|
{
|
||||||
|
char sd[600];
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
FILE *f = fopen(sd, "rb");
|
||||||
|
if (!f) { reply(ctrl, "550 Datei nicht gefunden"); return; }
|
||||||
|
reply(ctrl, "150 Sende Datei");
|
||||||
|
char *buf = malloc(XFER_SIZE);
|
||||||
|
bool ok = buf != NULL;
|
||||||
|
size_t n;
|
||||||
|
while (ok && (n = fread(buf, 1, XFER_SIZE, f)) > 0) ok = send_all(data, buf, n);
|
||||||
|
free(buf);
|
||||||
|
fclose(f);
|
||||||
|
reply(ctrl, ok ? "226 Fertig" : "426 Übertragung abgebrochen");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void store(int ctrl, int data, const char *path)
|
||||||
|
{
|
||||||
|
char sd[600];
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
FILE *f = fopen(sd, "wb");
|
||||||
|
if (!f) { reply(ctrl, "550 Datei nicht schreibbar"); return; }
|
||||||
|
reply(ctrl, "150 Empfange Datei");
|
||||||
|
char *buf = malloc(XFER_SIZE);
|
||||||
|
bool ok = buf != NULL;
|
||||||
|
ssize_t n;
|
||||||
|
while (ok && (n = recv(data, buf, XFER_SIZE, 0)) > 0) ok = fwrite(buf, 1, n, f) == (size_t)n;
|
||||||
|
free(buf);
|
||||||
|
fclose(f);
|
||||||
|
reply(ctrl, ok ? "226 Fertig" : "451 Schreiben fehlgeschlagen");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void session(int ctrl)
|
||||||
|
{
|
||||||
|
char cwd[512] = "/", line[600], path[512], sd[600], renameFrom[512] = "";
|
||||||
|
int pasvFd = -1;
|
||||||
|
size_t used = 0;
|
||||||
|
reply(ctrl, "220 MBMNET-FTP (ohne Passwort, Schreiben nur in " WRITE_ROOT ")");
|
||||||
|
|
||||||
|
while (!ftp_should_exit()) {
|
||||||
|
// Eine Zeile lesen
|
||||||
|
char *eol;
|
||||||
|
while (!(eol = memchr(line, '\n', used))) {
|
||||||
|
struct pollfd p = { .fd = ctrl, .events = POLLIN };
|
||||||
|
if (ftp_should_exit()) goto done;
|
||||||
|
int r = poll(&p, 1, 500);
|
||||||
|
if (r < 0) goto done;
|
||||||
|
if (r == 0) continue;
|
||||||
|
if (used >= sizeof(line) - 1) used = 0; // zu lange Zeile verwerfen
|
||||||
|
ssize_t n = recv(ctrl, line + used, sizeof(line) - 1 - used, 0);
|
||||||
|
if (n <= 0) goto done;
|
||||||
|
used += n;
|
||||||
|
}
|
||||||
|
*eol = 0;
|
||||||
|
if (eol > line && eol[-1] == '\r') eol[-1] = 0;
|
||||||
|
char cmd[600];
|
||||||
|
snprintf(cmd, sizeof(cmd), "%s", line);
|
||||||
|
size_t rest = used - (eol - line + 1);
|
||||||
|
memmove(line, eol + 1, rest);
|
||||||
|
used = rest;
|
||||||
|
|
||||||
|
char *arg = strchr(cmd, ' ');
|
||||||
|
if (arg) *arg++ = 0;
|
||||||
|
for (char *c = cmd; *c; c++) if (*c >= 'a' && *c <= 'z') *c -= 32;
|
||||||
|
|
||||||
|
if (!strcmp(cmd, "USER")) reply(ctrl, "331 Passwort egal");
|
||||||
|
else if (!strcmp(cmd, "PASS")) reply(ctrl, "230 Angemeldet");
|
||||||
|
else if (!strcmp(cmd, "SYST")) reply(ctrl, "215 UNIX Type: L8");
|
||||||
|
else if (!strcmp(cmd, "FEAT")) { reply(ctrl, "211-Features:\r\n PASV\r\n EPSV\r\n SIZE\r\n MDTM\r\n MLST type*;size*;modify*;perm*;\r\n UTF8\r\n211 Ende"); }
|
||||||
|
else if (!strcmp(cmd, "OPTS")) reply(ctrl, "200 OK");
|
||||||
|
else if (!strcmp(cmd, "NOOP")) reply(ctrl, "200 OK");
|
||||||
|
else if (!strcmp(cmd, "TYPE") || !strcmp(cmd, "MODE") || !strcmp(cmd, "STRU")) reply(ctrl, "200 OK");
|
||||||
|
else if (!strcmp(cmd, "PWD") || !strcmp(cmd, "XPWD")) reply(ctrl, "257 \"%s\"", cwd);
|
||||||
|
else if (!strcmp(cmd, "CWD") || !strcmp(cmd, "CDUP")) {
|
||||||
|
resolve(cwd, !strcmp(cmd, "CDUP") ? ".." : arg, path, sizeof(path));
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
struct stat st;
|
||||||
|
if (stat(sd, &st) == 0 && S_ISDIR(st.st_mode)) { snprintf(cwd, sizeof(cwd), "%s", path); reply(ctrl, "250 OK"); }
|
||||||
|
else if (!strcmp(path, "/")) { snprintf(cwd, sizeof(cwd), "/"); reply(ctrl, "250 OK"); }
|
||||||
|
else reply(ctrl, "550 Ordner nicht gefunden");
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "PASV") || !strcmp(cmd, "EPSV")) {
|
||||||
|
if (pasvFd >= 0) close(pasvFd);
|
||||||
|
pasvFd = open_passive(ctrl, !strcmp(cmd, "EPSV"));
|
||||||
|
if (pasvFd < 0) reply(ctrl, "425 Kein Daten-Port frei");
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "SIZE")) {
|
||||||
|
resolve(cwd, arg, path, sizeof(path));
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
struct stat st;
|
||||||
|
if (stat(sd, &st) == 0 && !S_ISDIR(st.st_mode)) reply(ctrl, "213 %llu", (unsigned long long)st.st_size);
|
||||||
|
else reply(ctrl, "550 Nicht gefunden");
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "MLST")) {
|
||||||
|
// Ein einzelner Eintrag, direkt auf der Steuer-Verbindung
|
||||||
|
resolve(cwd, arg, path, sizeof(path));
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
struct stat st = { 0 };
|
||||||
|
bool root = !strcmp(path, "/");
|
||||||
|
if (!root && stat(sd, &st) != 0) { reply(ctrl, "550 Nicht gefunden"); continue; }
|
||||||
|
char fact[700];
|
||||||
|
mlsx_fact(fact, sizeof(fact), &st, root || S_ISDIR(st.st_mode), path);
|
||||||
|
fact[strcspn(fact, "\r\n")] = 0;
|
||||||
|
reply(ctrl, "250-Eintrag\r\n %s\r\n250 Ende", fact);
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "MDTM")) {
|
||||||
|
resolve(cwd, arg, path, sizeof(path));
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
struct stat st;
|
||||||
|
char modify[20] = "20000101000000";
|
||||||
|
if (stat(sd, &st) != 0) { reply(ctrl, "550 Nicht gefunden"); continue; }
|
||||||
|
if (st.st_mtime > 0) strftime(modify, sizeof(modify), "%Y%m%d%H%M%S", gmtime(&st.st_mtime));
|
||||||
|
reply(ctrl, "213 %s", modify);
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "LIST") || !strcmp(cmd, "NLST") || !strcmp(cmd, "MLSD") || !strcmp(cmd, "RETR") || !strcmp(cmd, "STOR")) {
|
||||||
|
bool isList = cmd[0] == 'L' || cmd[0] == 'N' || cmd[0] == 'M';
|
||||||
|
// "LIST -la" u. ä.: Optionen ignorieren
|
||||||
|
const char *target = arg && arg[0] == '-' ? NULL : arg;
|
||||||
|
resolve(cwd, target, path, sizeof(path));
|
||||||
|
if (!strcmp(cmd, "STOR") && !writable(path)) { reply(ctrl, "550 Schreiben nur in " WRITE_ROOT); continue; }
|
||||||
|
if (pasvFd < 0) { reply(ctrl, "425 Erst PASV senden"); continue; }
|
||||||
|
int data = accept_data(pasvFd);
|
||||||
|
close(pasvFd);
|
||||||
|
pasvFd = -1;
|
||||||
|
if (data < 0) { reply(ctrl, "425 Keine Daten-Verbindung"); continue; }
|
||||||
|
if (isList) list_dir(ctrl, data, path, cmd[0] == 'N' ? LIST_NAMES : cmd[0] == 'M' ? LIST_MLSD : LIST_UNIX);
|
||||||
|
else if (cmd[0] == 'R') retrieve(ctrl, data, path);
|
||||||
|
else store(ctrl, data, path);
|
||||||
|
close(data);
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "DELE") || !strcmp(cmd, "RMD") || !strcmp(cmd, "MKD") || !strcmp(cmd, "XMKD")) {
|
||||||
|
resolve(cwd, arg, path, sizeof(path));
|
||||||
|
if (!writable(path) || !strcmp(path, WRITE_ROOT)) { reply(ctrl, "550 Ändern nur in " WRITE_ROOT); continue; }
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
int r = !strcmp(cmd, "DELE") ? remove(sd) : !strcmp(cmd, "RMD") ? rmdir(sd) : mkdir(sd, 0777);
|
||||||
|
if (r == 0) reply(ctrl, cmd[0] == 'M' || cmd[0] == 'X' ? "257 \"%s\" angelegt" : "250 OK", path);
|
||||||
|
else reply(ctrl, "550 Fehlgeschlagen");
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "RNFR")) {
|
||||||
|
resolve(cwd, arg, renameFrom, sizeof(renameFrom));
|
||||||
|
if (!writable(renameFrom)) { renameFrom[0] = 0; reply(ctrl, "550 Ändern nur in " WRITE_ROOT); }
|
||||||
|
else reply(ctrl, "350 Weiter mit RNTO");
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "RNTO")) {
|
||||||
|
resolve(cwd, arg, path, sizeof(path));
|
||||||
|
char from[600];
|
||||||
|
sd_path(renameFrom, from, sizeof(from));
|
||||||
|
sd_path(path, sd, sizeof(sd));
|
||||||
|
if (renameFrom[0] && writable(path) && rename(from, sd) == 0) reply(ctrl, "250 OK");
|
||||||
|
else reply(ctrl, "550 Umbenennen fehlgeschlagen");
|
||||||
|
renameFrom[0] = 0;
|
||||||
|
}
|
||||||
|
else if (!strcmp(cmd, "QUIT")) { reply(ctrl, "221 Tschüss"); break; }
|
||||||
|
else reply(ctrl, "502 Nicht unterstützt");
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
if (pasvFd >= 0) close(pasvFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nimmt Verbindungen an, bis das Netz weg ist, sich die IP ändert oder das Modul aufhören soll.
|
||||||
|
void ftp_serve(int listenFd, u32 ip)
|
||||||
|
{
|
||||||
|
while (!ftp_should_exit()) {
|
||||||
|
if ((u32)gethostid() != ip) return; // WLAN gewechselt oder weg
|
||||||
|
struct pollfd p = { .fd = listenFd, .events = POLLIN };
|
||||||
|
int r = poll(&p, 1, 2000);
|
||||||
|
if (r < 0 || (p.revents & (POLLERR | POLLHUP | POLLNVAL))) return;
|
||||||
|
if (r == 0) continue;
|
||||||
|
int ctrl = accept(listenFd, NULL, NULL);
|
||||||
|
if (ctrl < 0) return;
|
||||||
|
session(ctrl);
|
||||||
|
close(ctrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
#define CONTROL_PORT 5000
|
||||||
|
|
||||||
|
// Nimmt FTP-Verbindungen an, bis das Netz weg ist oder sich die IP ändert
|
||||||
|
void ftp_serve(int listenFd, u32 ip);
|
||||||
|
|
||||||
|
// Soll das Modul aufhören? (Stopp-Datei der App oder Beenden-Aufforderung des Systems; in main.c)
|
||||||
|
bool ftp_should_exit(void);
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
// Statusblock des FTP-Moduls. Die App liest ihn per Debug-Zugriff direkt aus dem Speicher des Moduls
|
||||||
|
// (die Adresse steht beim Bauen fest) - so klappt die Anzeige auch, wenn das Modul nicht an die SD kommt.
|
||||||
|
// Über "stop" kann die App das Modul ebenfalls ohne SD zum Beenden auffordern.
|
||||||
|
// Wird von ftpmodul/source/main.c und source/ftpctl.c (App) benutzt.
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
#define FTP_STATUS_MAGIC 0x5446424D // "MBFT"
|
||||||
|
|
||||||
|
enum {
|
||||||
|
FTP_STAGE_START = 0, // vor __appInit / srvInit
|
||||||
|
FTP_STAGE_INIT, // srv, fs, SD erledigt (Ergebnisse in srv/fs/sdmc)
|
||||||
|
FTP_STAGE_SOC, // wartet auf den Netzwerk-Dienst (socInit, Ergebnis in soc)
|
||||||
|
FTP_STAGE_WIFI, // wartet auf WLAN (keine IP)
|
||||||
|
FTP_STAGE_PORT, // Port 5000 lässt sich nicht öffnen (errno in bindErrno)
|
||||||
|
FTP_STAGE_READY, // bereit, IP in ip
|
||||||
|
FTP_STAGE_EXIT, // beendet sich
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
u32 magic;
|
||||||
|
u32 stage;
|
||||||
|
s32 srv, fs, sdmc, soc;
|
||||||
|
u32 ip;
|
||||||
|
u32 heartbeat; // zählt hoch, solange das Modul lebt
|
||||||
|
u32 stop; // die App setzt 1: Modul beendet sich sauber
|
||||||
|
s32 bindErrno;
|
||||||
|
} FtpStatus;
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
// MBMNET-FTP-Modul: läuft als eigener Hintergrund-Prozess, auch wenn die MBMNET-App geschlossen ist.
|
||||||
|
//
|
||||||
|
// Die App legt dieses Modul als /luma/sysmodules/0004013000F1E002.cxi ab und startet es über den
|
||||||
|
// Prozess-Manager (Luma lädt Systemmodule mit dieser Kennung von der SD). "FTP aus" in der App
|
||||||
|
// beendet den Prozess. Nach einem Neustart des 3DS ist es aus, bis die App es wieder startet.
|
||||||
|
//
|
||||||
|
// Die aktuelle Adresse steht für die App in /3ds/mbmnet/ftp-adresse.txt.
|
||||||
|
//
|
||||||
|
// Beenden immer sauber durch das Modul selbst (nie von außen abschießen - das brachte den 3DS zum
|
||||||
|
// Absturz): "FTP aus" legt /3ds/mbmnet/ftp-stopp an, beim Neustart/Ausschalten schickt das System
|
||||||
|
// die Benachrichtigung 0x100. Dann Verbindungen und Netzwerk-Dienst schließen und zurückkehren.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <malloc.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "ftp.h"
|
||||||
|
#include "ftpstatus.h"
|
||||||
|
|
||||||
|
// Statusblock für die App (Adresse fest, siehe Makefile -> ftpmodul.sym)
|
||||||
|
volatile FtpStatus ftpStatus __attribute__((used, aligned(4))) = { .magic = FTP_STATUS_MAGIC };
|
||||||
|
|
||||||
|
#define SOC_BUFFER_SIZE 0x20000
|
||||||
|
#define ADDRESS_FILE "sdmc:/3ds/mbmnet/ftp-adresse.txt"
|
||||||
|
#define LOG_FILE "sdmc:/3ds/mbmnet/ftp.log"
|
||||||
|
#define STOP_FILE "sdmc:/3ds/mbmnet/ftp-stopp"
|
||||||
|
|
||||||
|
static Handle notificationSemaphore;
|
||||||
|
static bool quit;
|
||||||
|
|
||||||
|
|
||||||
|
// Protokoll für die Fehlersuche (die App zeigt die letzte Zeile, wenn der Server nicht antwortet)
|
||||||
|
static void logf_(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(LOG_FILE, "a");
|
||||||
|
if (!f) return;
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vfprintf(f, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
fputc('\n', f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein Hintergrund-Modul bekommt nur wenig Speicher: Heap fest vorgeben statt "alles, was frei ist"
|
||||||
|
u32 __ctru_heap_size = 0x80000;
|
||||||
|
u32 __ctru_linear_heap_size = 0x1000;
|
||||||
|
|
||||||
|
// Nur die Dienste, die das Modul braucht (kein Grafik-/HOME-Menü-Dienst wie bei einer App)
|
||||||
|
void __appInit(void)
|
||||||
|
{
|
||||||
|
// srv ist beim Start evtl. noch belegt: kurz wiederholen
|
||||||
|
for (int i = 0; i < 100 && R_FAILED(ftpStatus.srv = srvInit()); i++) svcSleepThread(50 * 1000 * 1000);
|
||||||
|
ftpStatus.fs = fsInit();
|
||||||
|
ftpStatus.sdmc = archiveMountSdmc();
|
||||||
|
ftpStatus.stage = FTP_STAGE_INIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void __appExit(void)
|
||||||
|
{
|
||||||
|
archiveUnmountAll();
|
||||||
|
fsExit();
|
||||||
|
srvExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ftp_should_exit(void)
|
||||||
|
{
|
||||||
|
ftpStatus.heartbeat++;
|
||||||
|
if (ftpStatus.stop) quit = true; // Stopp-Bit der App (geht auch ohne SD)
|
||||||
|
if (quit) return true;
|
||||||
|
// Beenden-Aufforderung des Systems (Neustart, Ausschalten)
|
||||||
|
if (notificationSemaphore && R_SUCCEEDED(svcWaitSynchronization(notificationSemaphore, 0))) {
|
||||||
|
u32 id = 0;
|
||||||
|
if (R_SUCCEEDED(srvReceiveNotification(&id)) && id == 0x100) quit = true;
|
||||||
|
}
|
||||||
|
// Stopp-Datei der App, höchstens alle 2 Sekunden nachsehen
|
||||||
|
static u64 lastCheck;
|
||||||
|
u64 now = osGetTime();
|
||||||
|
if (!quit && now - lastCheck >= 2000) {
|
||||||
|
lastCheck = now;
|
||||||
|
struct stat st;
|
||||||
|
if (stat(STOP_FILE, &st) == 0) quit = true;
|
||||||
|
}
|
||||||
|
return quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wartet ms Millisekunden, bricht aber ab, sobald das Modul aufhören soll
|
||||||
|
static void sleep_ms(int ms)
|
||||||
|
{
|
||||||
|
for (int waited = 0; waited < ms && !ftp_should_exit(); waited += 100) svcSleepThread(100LL * 1000 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_address(u32 ip)
|
||||||
|
{
|
||||||
|
mkdir("sdmc:/3ds", 0777);
|
||||||
|
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||||
|
FILE *f = fopen(ADDRESS_FILE, "w");
|
||||||
|
if (!f) return;
|
||||||
|
if (ip) fprintf(f, "%lu.%lu.%lu.%lu:%d\n", (unsigned long)(ip & 0xFF), (unsigned long)((ip >> 8) & 0xFF),
|
||||||
|
(unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24), CONTROL_PORT);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
remove(LOG_FILE);
|
||||||
|
remove(STOP_FILE);
|
||||||
|
srvEnableNotification(¬ificationSemaphore); // für die Beenden-Aufforderung 0x100
|
||||||
|
logf_("MBMNET-FTP-Modul gestartet: srv %08lX, fs %08lX, sdmc %08lX, frei (Base) %lld KB",
|
||||||
|
(unsigned long)ftpStatus.srv, (unsigned long)ftpStatus.fs, (unsigned long)ftpStatus.sdmc,
|
||||||
|
osGetMemRegionFree(MEMREGION_BASE) / 1024);
|
||||||
|
u32 *socBuffer = memalign(0x1000, SOC_BUFFER_SIZE);
|
||||||
|
if (!socBuffer) { logf_("Fehler: kein Speicher für den Netzwerk-Puffer"); return 1; }
|
||||||
|
|
||||||
|
while (!ftp_should_exit()) {
|
||||||
|
// Netzwerk-Dienst (nach dem Schlafmodus ggf. neu)
|
||||||
|
Result rc;
|
||||||
|
int tries = 0;
|
||||||
|
ftpStatus.stage = FTP_STAGE_SOC;
|
||||||
|
while (R_FAILED(rc = ftpStatus.soc = socInit(socBuffer, SOC_BUFFER_SIZE))) {
|
||||||
|
if (ftp_should_exit()) goto done;
|
||||||
|
if (tries++ % 15 == 0) logf_("Netzwerk-Dienst (socInit) fehlgeschlagen: %08lX", (unsigned long)rc);
|
||||||
|
sleep_ms(2000);
|
||||||
|
}
|
||||||
|
logf_("Netzwerk-Dienst bereit");
|
||||||
|
|
||||||
|
int failures = 0, waits = 0;
|
||||||
|
while (failures < 5 && !ftp_should_exit()) {
|
||||||
|
u32 ip = (u32)gethostid();
|
||||||
|
ftpStatus.ip = ip;
|
||||||
|
if (!ip) { // noch kein WLAN
|
||||||
|
ftpStatus.stage = FTP_STAGE_WIFI;
|
||||||
|
if (waits++ % 15 == 0) logf_("Warte auf WLAN (keine IP)");
|
||||||
|
write_address(0);
|
||||||
|
sleep_ms(2000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
waits = 0;
|
||||||
|
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(CONTROL_PORT), .sin_addr.s_addr = INADDR_ANY };
|
||||||
|
if (fd < 0 || bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(fd, 2) != 0) {
|
||||||
|
logf_("Port %d öffnen fehlgeschlagen (Socket %d, errno %d)", CONTROL_PORT, fd, errno);
|
||||||
|
ftpStatus.stage = FTP_STAGE_PORT;
|
||||||
|
ftpStatus.bindErrno = errno;
|
||||||
|
if (fd >= 0) close(fd);
|
||||||
|
failures++;
|
||||||
|
sleep_ms(2000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
failures = 0;
|
||||||
|
ftpStatus.stage = FTP_STAGE_READY;
|
||||||
|
write_address(ip);
|
||||||
|
logf_("Bereit: ftp://%lu.%lu.%lu.%lu:%d", (unsigned long)(ip & 0xFF), (unsigned long)((ip >> 8) & 0xFF),
|
||||||
|
(unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24), CONTROL_PORT);
|
||||||
|
ftp_serve(fd, ip); // kehrt zurück, wenn das Netz weg ist oder sich die IP ändert
|
||||||
|
close(fd);
|
||||||
|
sleep_ms(500);
|
||||||
|
}
|
||||||
|
socExit();
|
||||||
|
if (ftp_should_exit()) break;
|
||||||
|
// Mehrfach kein Socket möglich (z. B. nach dem Schlafmodus): Netzwerk-Dienst neu aufsetzen
|
||||||
|
logf_("Netzwerk-Dienst wird neu aufgesetzt");
|
||||||
|
sleep_ms(2000);
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
ftpStatus.stage = FTP_STAGE_EXIT;
|
||||||
|
// Sauber beenden: Adresse weg, Stopp-Datei weg; libctru schließt danach fs und srv
|
||||||
|
remove(ADDRESS_FILE);
|
||||||
|
remove(STOP_FILE);
|
||||||
|
logf_("Beendet");
|
||||||
|
free(socBuffer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Baut die App und lädt sie ins Repo "MBMNET" hoch. Danach aktualisiert sich jede
|
||||||
|
# MBMNET-App beim nächsten Start von selbst.
|
||||||
|
# Zusätzlich legt es auf Gitea ein Release "vX.Y.Z" mit .cia und .3dsx an (für Universal-Updater).
|
||||||
|
#
|
||||||
|
# Vorher in Makefile die VERSION erhöhen!
|
||||||
|
# ./release.sh ["Release-Notizen"] bauen, hochladen, Release anlegen
|
||||||
|
# ./release.sh --nur-release ["Notizen"] nur das Release zur aktuellen Version anlegen
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
VERSION=$(sed -n 's/^VERSION *:= *//p' Makefile)
|
||||||
|
RELEASE=../release
|
||||||
|
GITEA=https://git.mbm.wtf
|
||||||
|
REPO=MrMaho/MBMNET
|
||||||
|
|
||||||
|
NUR_RELEASE=0
|
||||||
|
if [ "${1:-}" = "--nur-release" ]; then NUR_RELEASE=1; shift; fi
|
||||||
|
NOTIZEN=${1:-"MBMNET-App v$VERSION"}
|
||||||
|
|
||||||
|
if [ "$NUR_RELEASE" = 0 ]; then
|
||||||
|
# Build-Umgebung mit makerom und bannertool (für die CIA), wird nur beim ersten Mal gebaut
|
||||||
|
docker image inspect mbmnet-3ds-build >/dev/null 2>&1 || docker build -t mbmnet-3ds-build tools
|
||||||
|
|
||||||
|
echo "Baue MBMNET-App v$VERSION ..."
|
||||||
|
docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c 'make clean >/dev/null && make && make cia'
|
||||||
|
|
||||||
|
cp mbmnet.3dsx mbmnet.cia "$RELEASE/"
|
||||||
|
echo "$VERSION" > "$RELEASE/version.txt"
|
||||||
|
|
||||||
|
(cd "$RELEASE" && git add mbmnet.3dsx mbmnet.cia version.txt && git commit -m "MBMNET-App v$VERSION" && git push)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Gitea-Release -------------------------------------------------------------------
|
||||||
|
# Zugangsdaten wie bei git push aus dem Schlüsselbund; sie landen nur in einer curl-Konfiguration
|
||||||
|
# über stdin (nicht in der Prozessliste, nicht in der Ausgabe).
|
||||||
|
CRED=$(printf 'protocol=https\nhost=git.mbm.wtf\n\n' | git credential fill)
|
||||||
|
GUSER=$(sed -n 's/^username=//p' <<<"$CRED")
|
||||||
|
GPASS=$(sed -n 's/^password=//p' <<<"$CRED")
|
||||||
|
api() { # api METHODE PFAD [weitere curl-Argumente]
|
||||||
|
local method=$1 path=$2; shift 2
|
||||||
|
printf 'user = "%s:%s"\n' "$GUSER" "$GPASS" | curl -sS --fail-with-body -K - -X "$method" "$GITEA/api/v1/repos/$REPO$path" "$@"
|
||||||
|
}
|
||||||
|
json() { python3 -c "import json,sys; d=json.load(sys.stdin); print($1)"; }
|
||||||
|
|
||||||
|
TAG="v$VERSION"
|
||||||
|
COMMIT=$(git -C "$RELEASE" rev-parse HEAD)
|
||||||
|
if ID=$( (api GET "/releases/tags/$TAG" | json "d['id']") 2>/dev/null ); then
|
||||||
|
echo "Release $TAG gibt es schon (ID $ID), ersetze die Dateien ..."
|
||||||
|
else
|
||||||
|
BODY=$(TAG="$TAG" COMMIT="$COMMIT" NOTIZEN="$NOTIZEN" python3 -c \
|
||||||
|
'import json,os; print(json.dumps({"tag_name": os.environ["TAG"], "target_commitish": os.environ["COMMIT"], "name": "MBMNET " + os.environ["TAG"], "body": os.environ["NOTIZEN"]}))')
|
||||||
|
ID=$(api POST /releases -H 'Content-Type: application/json' -d "$BODY" | json "d['id']")
|
||||||
|
echo "Release $TAG angelegt (ID $ID)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vorhandene Dateien gleichen Namens löschen (bei erneutem Lauf), dann hochladen
|
||||||
|
for f in mbmnet.cia mbmnet.3dsx; do
|
||||||
|
for old in $(api GET "/releases/$ID/assets" | json "' '.join(str(a['id']) for a in d if a['name']=='$f')"); do
|
||||||
|
api DELETE "/releases/$ID/assets/$old" >/dev/null
|
||||||
|
done
|
||||||
|
api POST "/releases/$ID/assets?name=$f" -F "attachment=@$RELEASE/$f" >/dev/null
|
||||||
|
echo " $f hochgeladen"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Fertig: v$VERSION ist veröffentlicht ($GITEA/$REPO/releases/tag/$TAG)."
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Entwickler-Werkzeuge nur für Admins: Die App fragt bei MBMNET, ob die verknüpfte NNID einem Admin
|
||||||
|
// gehört (GET /mbmnet/app-info?pid=...). Die Antwort wird auf der SD gemerkt, damit die Werkzeuge
|
||||||
|
// auch im Nintendo-Modus und ohne Verbindung da sind.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "admin.h"
|
||||||
|
|
||||||
|
#define CACHE_FILE "sdmc:/3ds/mbmnet/admin"
|
||||||
|
|
||||||
|
static volatile bool isAdmin;
|
||||||
|
static Thread thread;
|
||||||
|
static char url[128];
|
||||||
|
|
||||||
|
static void remember(bool on)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(CACHE_FILE, "w");
|
||||||
|
if (f) { fputs(on ? "1" : "0", f); fclose(f); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static void worker(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
httpcContext ctx;
|
||||||
|
if (R_FAILED(httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1))) return;
|
||||||
|
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
|
||||||
|
u32 status = 0, got = 0;
|
||||||
|
char body[32] = { 0 };
|
||||||
|
Result rc = httpcBeginRequest(&ctx);
|
||||||
|
if (R_SUCCEEDED(rc)) rc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 8ULL * 1000 * 1000 * 1000);
|
||||||
|
if (R_SUCCEEDED(rc) && status == 200) httpcDownloadData(&ctx, (u8 *)body, sizeof(body) - 1, &got);
|
||||||
|
httpcCloseContext(&ctx);
|
||||||
|
if (status != 200 || strncmp(body, "admin=", 6) != 0) return; // keine Antwort: gemerkte gilt weiter
|
||||||
|
isAdmin = body[6] == '1';
|
||||||
|
remember(isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void admin_start(const char *server, u32 pid)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(CACHE_FILE, "r");
|
||||||
|
if (f) { isAdmin = fgetc(f) == '1'; fclose(f); }
|
||||||
|
if (!pid) return;
|
||||||
|
snprintf(url, sizeof(url), "http://%s/mbmnet/app-info?pid=%lu", server, (unsigned long)pid);
|
||||||
|
s32 prio = 0x30;
|
||||||
|
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||||
|
thread = threadCreate(worker, NULL, 16 * 1024, prio + 1, -2, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool admin_is(void) { return isAdmin; }
|
||||||
|
|
||||||
|
void admin_finish(void)
|
||||||
|
{
|
||||||
|
if (!thread) return;
|
||||||
|
if (R_SUCCEEDED(threadJoin(thread, 3ULL * 1000 * 1000 * 1000))) threadFree(thread);
|
||||||
|
thread = NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
// Fragt im Hintergrund bei MBMNET, ob die verknüpfte NNID (pid) einem Admin gehört.
|
||||||
|
// pid = 0: nicht fragen, nur die gemerkte Antwort vom letzten Mal benutzen.
|
||||||
|
void admin_start(const char *server, u32 pid);
|
||||||
|
// Admin? (gemerkte Antwort, bis der Server geantwortet hat)
|
||||||
|
bool admin_is(void);
|
||||||
|
void admin_finish(void);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Entpacker für das "Backwards LZ"-Format, mit dem Nintendo die .code von
|
||||||
|
// Systemmodulen komprimiert. Luma3DS wendet code.ips-Patches auf die
|
||||||
|
// ENTPACKTE .code an, deshalb müssen wir genauso rechnen.
|
||||||
|
#include <string.h>
|
||||||
|
#include "blz.h"
|
||||||
|
|
||||||
|
static u32 read32(const u8 *p) { return p[0] | (p[1] << 8) | (p[2] << 16) | ((u32)p[3] << 24); }
|
||||||
|
|
||||||
|
bool blz_looks_compressed(const u8 *buf, u32 size)
|
||||||
|
{
|
||||||
|
if (size < 8) return false;
|
||||||
|
u32 enc = read32(buf + size - 8);
|
||||||
|
u32 add = read32(buf + size - 4);
|
||||||
|
u32 hdr = enc >> 24;
|
||||||
|
u32 enclen = enc & 0xFFFFFF;
|
||||||
|
return hdr >= 8 && hdr <= 0x10 && enclen >= hdr && enclen <= size && add > 0 && add < 0x1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 blz_decompressed_size(const u8 *buf, u32 size)
|
||||||
|
{
|
||||||
|
return size + read32(buf + size - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// buf muss mindestens blz_decompressed_size() Bytes groß sein.
|
||||||
|
// Daten liegen am Anfang von buf, es wird rückwärts in-place entpackt.
|
||||||
|
bool blz_decompress(u8 *buf, u32 size)
|
||||||
|
{
|
||||||
|
u32 enc = read32(buf + size - 8);
|
||||||
|
u32 hdr = enc >> 24;
|
||||||
|
u32 enclen = enc & 0xFFFFFF;
|
||||||
|
u32 outsize = blz_decompressed_size(buf, size);
|
||||||
|
|
||||||
|
u8 *src = buf + size - hdr;
|
||||||
|
u8 *srcEnd = buf + size - enclen;
|
||||||
|
u8 *dst = buf + outsize;
|
||||||
|
|
||||||
|
while (src > srcEnd) {
|
||||||
|
u8 flags = *--src;
|
||||||
|
for (int i = 0; i < 8 && src > srcEnd; i++, flags <<= 1) {
|
||||||
|
if (flags & 0x80) {
|
||||||
|
if (src - 2 < buf) return false;
|
||||||
|
u8 hi = *--src;
|
||||||
|
u8 lo = *--src;
|
||||||
|
u32 len = (hi >> 4) + 3;
|
||||||
|
u32 disp = (((hi & 0x0F) << 8) | lo) + 3;
|
||||||
|
for (u32 j = 0; j < len; j++) {
|
||||||
|
if (dst - 1 < buf || dst - 1 + disp >= buf + outsize) return false;
|
||||||
|
dst--;
|
||||||
|
*dst = dst[disp];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (dst - 1 < buf) return false;
|
||||||
|
*--dst = *--src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
bool blz_looks_compressed(const u8 *buf, u32 size);
|
||||||
|
u32 blz_decompressed_size(const u8 *buf, u32 size);
|
||||||
|
bool blz_decompress(u8 *buf, u32 size);
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
// Eigenes MBMNET-Freundekonto, wie bei Pretendos Nimbus:
|
||||||
|
//
|
||||||
|
// Freundekonto 1: Nintendo - mit Konto-Platz 1 (Nintendo Network ID) und der Nintendo-Freundesliste
|
||||||
|
// Freundekonto 3: MBMNET - mit eigenem Konto-Platz (MBMNET-Konto) und eigener Freundesliste
|
||||||
|
//
|
||||||
|
// (Freundekonto 2 benutzt Pretendos Nimbus, das bleibt unangetastet.)
|
||||||
|
//
|
||||||
|
// Umschalten: beide Konten abmelden, das andere laden, seinen Konto-Platz zum Standard machen, Neustart.
|
||||||
|
// Beim ersten Mal legt die App das MBMNET-Freundekonto an. Es hat noch keine Kennung; beim ersten
|
||||||
|
// Login holt es sich bei MBMNET ein neues Konto (NASC "passwd"), der Server gibt ihm den Freundescode
|
||||||
|
// des MBMNET-Kontos von der Webseite.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "frdswitch.h"
|
||||||
|
|
||||||
|
#define RESULT_LOCAL_ACCOUNT_NOT_EXISTS 0xC880C4ED
|
||||||
|
#define LOG_FILE "sdmc:/3ds/mbmnet/konto.log"
|
||||||
|
|
||||||
|
static bool ready;
|
||||||
|
static FILE *logf;
|
||||||
|
static const char *failedStep;
|
||||||
|
|
||||||
|
static Result step(const char *name, Result rc)
|
||||||
|
{
|
||||||
|
if (logf) fprintf(logf, "%-36s %08lX%s\n", name, (unsigned long)rc, R_FAILED(rc) ? " <- FEHLER" : "");
|
||||||
|
if (R_FAILED(rc) && !failedStep) failedStep = name;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void note(const char *name, Result rc)
|
||||||
|
{
|
||||||
|
if (logf) fprintf(logf, "%-36s %08lX%s\n", name, (unsigned long)rc, R_FAILED(rc) ? " (ignoriert)" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
Result frdswitch_init(void)
|
||||||
|
{
|
||||||
|
Result rc = ndmuInit();
|
||||||
|
if (R_SUCCEEDED(rc)) rc = frdInit(false); // frd:a (Verwaltung)
|
||||||
|
if (R_SUCCEEDED(rc)) rc = actInit(false); // act:a
|
||||||
|
// Ohne diese SDK-Version lehnt der Freunde-Dienst Anlegen und Umschalten ab (so macht es Nimbus;
|
||||||
|
// daran scheiterte unser erster Versuch mit C960C4F6)
|
||||||
|
if (R_SUCCEEDED(rc)) rc = FRD_SetClientSdkVersion(0x70000c8);
|
||||||
|
ready = R_SUCCEEDED(rc);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void frdswitch_exit(void)
|
||||||
|
{
|
||||||
|
actExit();
|
||||||
|
frdExit();
|
||||||
|
ndmuExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 frdswitch_current(void)
|
||||||
|
{
|
||||||
|
u8 id = 0;
|
||||||
|
if (ready) FRD_GetMyLocalAccountId(&id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konto-Platz (act) zu einem Freundekonto. 0 = keiner.
|
||||||
|
static u32 act_slot_of(u8 friendAccount)
|
||||||
|
{
|
||||||
|
u8 count = 0;
|
||||||
|
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return 0;
|
||||||
|
for (u8 slot = 1; slot <= count; slot++) {
|
||||||
|
u8 fp = 0;
|
||||||
|
if (R_SUCCEEDED(ACT_GetAccountInfo(&fp, sizeof(fp), slot, INFO_TYPE_FP_LOCAL_ACCOUNT_ID)) && fp == friendAccount) return slot;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool frdswitch_slot_linked(u8 friendAccount, char *id, size_t len)
|
||||||
|
{
|
||||||
|
id[0] = 0;
|
||||||
|
u32 slot = ready ? act_slot_of(friendAccount) : 0;
|
||||||
|
if (!slot) return false;
|
||||||
|
AccountId acc = { 0 };
|
||||||
|
if (R_FAILED(ACT_GetAccountInfo(acc, sizeof(acc), slot, INFO_TYPE_ACCOUNT_ID))) return false;
|
||||||
|
snprintf(id, len, "%s", acc);
|
||||||
|
return id[0] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beide Konten abmelden. Das Freundekonto lässt sich nur offline entladen.
|
||||||
|
static Result unload(void)
|
||||||
|
{
|
||||||
|
note("Konto (act) abmelden", ACTA_UnloadConsoleAccount()); // ohne geladenes Konto: "ungültiger Zustand"
|
||||||
|
|
||||||
|
Result rc = step("Offline gehen (exklusiv)", NDMU_EnterExclusiveState(NDM_EXCLUSIVE_STATE_LOCAL_COMMUNICATIONS));
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
for (int i = 0; i < 150; i++) { // höchstens 15 Sekunden
|
||||||
|
bool online = true;
|
||||||
|
if (R_FAILED(FRD_IsOnline(&online)) || !online) break;
|
||||||
|
svcSleepThread(100 * 1000 * 1000);
|
||||||
|
}
|
||||||
|
// Hängt der Freunde-Dienst noch an einer Anfrage, kurz wiederholen
|
||||||
|
for (int attempt = 1; attempt <= 20; attempt++) {
|
||||||
|
rc = FRDA_UnloadLocalAccount();
|
||||||
|
if (R_SUCCEEDED(rc)) {
|
||||||
|
if (logf) fprintf(logf, "%-36s %08lX (Versuch %d)\n", "Freundekonto abmelden", (unsigned long)rc, attempt);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
svcSleepThread(500 * 1000 * 1000);
|
||||||
|
}
|
||||||
|
return step("Freundekonto abmelden (10 s)", rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Freundekonto laden. "Schon geladen" (z. B. direkt nach dem Anlegen) zählt als Erfolg.
|
||||||
|
static Result load_friend_account(u8 friendAccount)
|
||||||
|
{
|
||||||
|
Result rc = FRDA_LoadLocalAccount(friendAccount);
|
||||||
|
if (R_FAILED(rc) && frdswitch_current() == friendAccount) {
|
||||||
|
note("Freundekonto laden (war schon geladen)", rc);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return step("Freundekonto laden", rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konto-Platz (act) des Freundekontos zum Standard machen; fehlt er, anlegen. *created: neu angelegt.
|
||||||
|
static Result use_act_slot(u8 friendAccount, u32 *slotOut, bool *created)
|
||||||
|
{
|
||||||
|
Result rc = 0;
|
||||||
|
u32 slot = act_slot_of(friendAccount);
|
||||||
|
*created = false;
|
||||||
|
if (!slot) {
|
||||||
|
u8 count = 0;
|
||||||
|
rc = step("Konto-Plätze zählen", ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS));
|
||||||
|
if (R_SUCCEEDED(rc)) rc = step("Konto-Platz anlegen", ACTA_CreateConsoleAccount());
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
slot = count + 1;
|
||||||
|
rc = step("Konto-Platz speichern", ACTA_CommitConsoleAccount(slot));
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
*created = true;
|
||||||
|
}
|
||||||
|
if (logf) fprintf(logf, " -> Konto-Platz %lu\n", (unsigned long)slot);
|
||||||
|
*slotOut = slot;
|
||||||
|
return step("Standard-Konto setzen", ACTA_SetDefaultAccount(slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool frdswitch_to(u8 target, bool *created, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
*created = false;
|
||||||
|
if (!ready) {
|
||||||
|
snprintf(msg, msglen, "Kontodienste nicht verfügbar.\nIst Luma3DS aktuell?");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||||
|
logf = fopen(LOG_FILE, "w");
|
||||||
|
failedStep = NULL;
|
||||||
|
u8 from = frdswitch_current();
|
||||||
|
if (logf) fprintf(logf, "MBMNET: Freundekonto %u -> %u (App v%s)\n\n", from, target, APP_VERSION);
|
||||||
|
|
||||||
|
u32 slot = 0;
|
||||||
|
bool newSlot = false;
|
||||||
|
Result rc = unload();
|
||||||
|
if (R_SUCCEEDED(rc)) {
|
||||||
|
rc = FRDA_LoadLocalAccount(target);
|
||||||
|
if (rc == (Result)RESULT_LOCAL_ACCOUNT_NOT_EXISTS && target == FRIEND_ACCOUNT_MBMNET) {
|
||||||
|
// Erstes Mal: eigenes MBMNET-Freundekonto anlegen (normale Umgebung, die Patches lenken sie um)
|
||||||
|
note("Freundekonto laden (gibt es noch nicht)", rc);
|
||||||
|
rc = step("MBMNET-Freundekonto anlegen", FRDA_CreateLocalAccount(FRIEND_ACCOUNT_MBMNET, 0, 0, 1));
|
||||||
|
if (R_SUCCEEDED(rc)) { *created = true; rc = load_friend_account(target); }
|
||||||
|
} else if (R_FAILED(rc) && frdswitch_current() == target) {
|
||||||
|
note("Freundekonto laden (war schon geladen)", rc);
|
||||||
|
rc = 0;
|
||||||
|
} else {
|
||||||
|
step("Freundekonto laden", rc);
|
||||||
|
}
|
||||||
|
if (R_SUCCEEDED(rc)) rc = use_act_slot(target, &slot, &newSlot);
|
||||||
|
// Neuer Platz: frisch zurücksetzen, damit "Vorhandene ID verknüpfen" angeboten wird (darf scheitern)
|
||||||
|
if (R_SUCCEEDED(rc) && newSlot) note("Konto-Platz zurücksetzen", ACTA_UnbindServerAccount(slot, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
// Zurück auf das vorherige Freundekonto, damit der 3DS nicht ohne/mit falschem Konto dasteht.
|
||||||
|
// Erst entladen - ein geladenes Konto lässt sich nicht einfach ersetzen.
|
||||||
|
u8 back = from ? from : FRIEND_ACCOUNT_NINTENDO;
|
||||||
|
if (frdswitch_current() != back) {
|
||||||
|
note("Rückweg: Freundekonto abmelden", FRDA_UnloadLocalAccount());
|
||||||
|
note("Rückweg: altes Freundekonto laden", FRDA_LoadLocalAccount(back));
|
||||||
|
}
|
||||||
|
NDMU_LeaveExclusiveState();
|
||||||
|
}
|
||||||
|
if (logf) { fprintf(logf, "\nErgebnis: %s\n", R_SUCCEEDED(rc) ? "OK" : "FEHLER"); fclose(logf); logf = NULL; }
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "Umschalten fehlgeschlagen bei:\n%s (Fehler %08lX)\nProtokoll: /3ds/mbmnet/konto.log",
|
||||||
|
failedStep ? failedStep : "?", (unsigned long)rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 frdswitch_my_pid(void)
|
||||||
|
{
|
||||||
|
FriendKey key = { 0 };
|
||||||
|
if (!ready || R_FAILED(FRD_GetMyFriendKey(&key))) return 0;
|
||||||
|
return key.principalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool frdswitch_pid_is_mbmnet(u32 pid)
|
||||||
|
{
|
||||||
|
return pid >= MBMNET_PID_FIRST && pid <= MBMNET_PID_LAST;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das MBMNET-Freundekonto neu anlegen (es hat eine Kennung von einem anderen Server, z. B. Pretendo).
|
||||||
|
// Der Konto-Platz mit der verknüpften MBMNET-ID bleibt, nur das Freundekonto wird ersetzt. Danach
|
||||||
|
// Neustart: Beim ersten Login holt es sich bei MBMNET eine neue Kennung.
|
||||||
|
bool frdswitch_recreate_mbmnet(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (!ready || frdswitch_current() != FRIEND_ACCOUNT_MBMNET) {
|
||||||
|
snprintf(msg, msglen, "MBMNET-Freundekonto ist nicht geladen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||||
|
logf = fopen(LOG_FILE, "w");
|
||||||
|
failedStep = NULL;
|
||||||
|
if (logf) fprintf(logf, "MBMNET: Freundekonto 3 neu anlegen, alte Kennung %lu (App v%s)\n\n",
|
||||||
|
(unsigned long)frdswitch_my_pid(), APP_VERSION);
|
||||||
|
|
||||||
|
u32 slot = 0;
|
||||||
|
bool newSlot = false;
|
||||||
|
Result rc = unload();
|
||||||
|
if (R_SUCCEEDED(rc)) rc = step("MBMNET-Freundekonto neu anlegen", FRDA_CreateLocalAccount(FRIEND_ACCOUNT_MBMNET, 0, 0, 1));
|
||||||
|
if (R_SUCCEEDED(rc)) rc = load_friend_account(FRIEND_ACCOUNT_MBMNET);
|
||||||
|
if (R_SUCCEEDED(rc)) rc = use_act_slot(FRIEND_ACCOUNT_MBMNET, &slot, &newSlot);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
if (frdswitch_current() != FRIEND_ACCOUNT_MBMNET) note("Rückweg: Freundekonto laden", FRDA_LoadLocalAccount(FRIEND_ACCOUNT_MBMNET));
|
||||||
|
NDMU_LeaveExclusiveState();
|
||||||
|
}
|
||||||
|
if (logf) { fprintf(logf, "\nErgebnis: %s\n", R_SUCCEEDED(rc) ? "OK" : "FEHLER"); fclose(logf); logf = NULL; }
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "Neu anlegen fehlgeschlagen bei:\n%s (Fehler %08lX)\nProtokoll: /3ds/mbmnet/konto.log",
|
||||||
|
failedStep ? failedStep : "?", (unsigned long)rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
u8 frdswitch_slot_of(u8 friendAccount)
|
||||||
|
{
|
||||||
|
return ready ? (u8)act_slot_of(friendAccount) : 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define FRIEND_ACCOUNT_NINTENDO 1
|
||||||
|
#define FRIEND_ACCOUNT_MBMNET 3 // 2 benutzt Pretendos Nimbus
|
||||||
|
|
||||||
|
// Kennungen (PIDs), die MBMNET für Freundekonten vergibt (FRIEND_PID_BASE in db.js)
|
||||||
|
#define MBMNET_PID_FIRST 1900000001u
|
||||||
|
#define MBMNET_PID_LAST 1999999999u
|
||||||
|
|
||||||
|
Result frdswitch_init(void);
|
||||||
|
void frdswitch_exit(void);
|
||||||
|
// Geladenes Freundekonto (1 = Nintendo, 3 = MBMNET, 0 = unbekannt)
|
||||||
|
u8 frdswitch_current(void);
|
||||||
|
// Ist im Konto-Platz dieses Freundekontos eine ID verknüpft? (Name in id)
|
||||||
|
bool frdswitch_slot_linked(u8 friendAccount, char *id, size_t len);
|
||||||
|
// Auf ein Freundekonto umschalten (MBMNET: beim ersten Mal anlegen, *created = true).
|
||||||
|
// Danach muss der 3DS neu starten. false + msg bei Fehler.
|
||||||
|
bool frdswitch_to(u8 target, bool *created, char *msg, size_t msglen);
|
||||||
|
// Kennung des geladenen Freundekontos (0 = noch keine)
|
||||||
|
u32 frdswitch_my_pid(void);
|
||||||
|
bool frdswitch_pid_is_mbmnet(u32 pid);
|
||||||
|
// MBMNET-Freundekonto neu anlegen (hat eine fremde Kennung). Danach Neustart.
|
||||||
|
bool frdswitch_recreate_mbmnet(char *msg, size_t msglen);
|
||||||
|
// Konto-Platz (act) eines Freundekontos, 0 = keiner
|
||||||
|
u8 frdswitch_slot_of(u8 friendAccount);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// FTP-Kern (Befehle, Pfade, Schreibschutz) aus dem FTP-Modul wiederverwenden - eine Quelle für beides
|
||||||
|
#include "../ftpmodul/source/ftp.c"
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
// FTP-Server in der App (Entwickler-Werkzeuge). Läuft in einem eigenen Thread, solange die App offen ist.
|
||||||
|
//
|
||||||
|
// Das Hintergrund-Modul (ftpmodul/, eigener Prozess auf Kern 1) ist abgeschaltet: Es brachte den
|
||||||
|
// Kernel-Scheduler auf Kern 1 zum Absturz (PC FFF21584, kaputter Thread in der Warteschlange). Die App
|
||||||
|
// räumt seine Dateien weg, damit es nie wieder startet.
|
||||||
|
//
|
||||||
|
// Ohne Passwort, Port 5000, Lesen von der ganzen SD, Schreiben/Löschen nur in /3ds/mbmnet (ftp.c).
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <malloc.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "ftpctl.h"
|
||||||
|
#include "../ftpmodul/source/ftp.h"
|
||||||
|
|
||||||
|
#define SOC_BUFFER_SIZE 0x100000
|
||||||
|
|
||||||
|
static u32 *socBuffer;
|
||||||
|
static Thread thread;
|
||||||
|
static volatile bool running;
|
||||||
|
static char address[32];
|
||||||
|
static char status[64];
|
||||||
|
|
||||||
|
bool ftp_should_exit(void) { return !running; }
|
||||||
|
|
||||||
|
static void server(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
while (running) {
|
||||||
|
u32 ip = (u32)gethostid();
|
||||||
|
if (!ip) { snprintf(status, sizeof(status), "kein WLAN"); address[0] = 0; svcSleepThread(1000000000LL); continue; }
|
||||||
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(CONTROL_PORT), .sin_addr.s_addr = INADDR_ANY };
|
||||||
|
if (fd < 0 || bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0 || listen(fd, 2) != 0) {
|
||||||
|
if (fd >= 0) close(fd);
|
||||||
|
snprintf(status, sizeof(status), "Port %d belegt", CONTROL_PORT);
|
||||||
|
svcSleepThread(2000000000LL);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
snprintf(address, sizeof(address), "%lu.%lu.%lu.%lu:%d", (unsigned long)(ip & 0xFF), (unsigned long)((ip >> 8) & 0xFF),
|
||||||
|
(unsigned long)((ip >> 16) & 0xFF), (unsigned long)(ip >> 24), CONTROL_PORT);
|
||||||
|
ftp_serve(fd, ip); // bis WLAN weg, IP neu oder "FTP aus"
|
||||||
|
close(fd);
|
||||||
|
address[0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reste des abgeschalteten Hintergrund-Moduls entfernen (sonst startete eine alte App-Version es wieder)
|
||||||
|
void ftpctl_cleanup_module(void)
|
||||||
|
{
|
||||||
|
remove("sdmc:/luma/sysmodules/0004013000F1E002.cxi");
|
||||||
|
remove("sdmc:/luma/sysmodules/0004013000F1E001.cxi");
|
||||||
|
remove("sdmc:/3ds/mbmnet/ftp-an");
|
||||||
|
remove("sdmc:/3ds/mbmnet/ftp-stopp");
|
||||||
|
remove("sdmc:/3ds/mbmnet/ftp-adresse.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ftpctl_running(void) { return running; }
|
||||||
|
|
||||||
|
bool ftpctl_start(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (running) return true;
|
||||||
|
socBuffer = memalign(0x1000, SOC_BUFFER_SIZE);
|
||||||
|
if (!socBuffer || R_FAILED(socInit(socBuffer, SOC_BUFFER_SIZE))) {
|
||||||
|
free(socBuffer);
|
||||||
|
socBuffer = NULL;
|
||||||
|
snprintf(msg, msglen, "FTP: Netzwerk nicht verfügbar.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
running = true;
|
||||||
|
status[0] = address[0] = 0;
|
||||||
|
s32 prio = 0x30;
|
||||||
|
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||||
|
thread = threadCreate(server, NULL, 32 * 1024, prio + 1, -2, false);
|
||||||
|
if (!thread) {
|
||||||
|
running = false;
|
||||||
|
socExit();
|
||||||
|
free(socBuffer);
|
||||||
|
socBuffer = NULL;
|
||||||
|
snprintf(msg, msglen, "FTP: konnte nicht starten.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
snprintf(msg, msglen, "FTP-Server an (läuft, solange die App offen ist).\nOhne Passwort, Port 5000. Herunterladen von der ganzen SD,\n"
|
||||||
|
"Hochladen/Löschen nur in /3ds/mbmnet.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ftpctl_stop(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (running) {
|
||||||
|
running = false;
|
||||||
|
threadJoin(thread, 5000000000LL);
|
||||||
|
threadFree(thread);
|
||||||
|
thread = NULL;
|
||||||
|
socExit();
|
||||||
|
free(socBuffer);
|
||||||
|
socBuffer = NULL;
|
||||||
|
}
|
||||||
|
address[0] = 0;
|
||||||
|
if (msg) snprintf(msg, msglen, "FTP-Server aus.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ftpctl_resume(char *msg, size_t msglen) { (void)msg; (void)msglen; return false; }
|
||||||
|
const char *ftpctl_address(void) { return address; }
|
||||||
|
const char *ftpctl_status(void) { return status[0] ? status : "startet ..."; }
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// FTP-Server in der App (läuft, solange die App offen ist)
|
||||||
|
bool ftpctl_running(void);
|
||||||
|
bool ftpctl_start(char *msg, size_t msglen);
|
||||||
|
void ftpctl_stop(char *msg, size_t msglen);
|
||||||
|
// Nach einem Neustart des 3DS: wieder starten, wenn FTP zuletzt an war. true = gestartet (msg gesetzt)
|
||||||
|
bool ftpctl_resume(char *msg, size_t msglen);
|
||||||
|
// "192.168.1.42:5000", leer solange das Modul noch keine Adresse hat
|
||||||
|
const char *ftpctl_address(void);
|
||||||
|
// Letzte Zeile aus dem Protokoll des Moduls (/3ds/mbmnet/ftp.log), leer wenn keins
|
||||||
|
const char *ftpctl_status(void);
|
||||||
|
// Reste des abgeschalteten Hintergrund-Moduls entfernen
|
||||||
|
void ftpctl_cleanup_module(void);
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// Ändert Optionen in Lumas Konfiguration (/luma/config.ini):
|
||||||
|
// splash_position -> Boot-Splash "MBMNET aktiv" anzeigen
|
||||||
|
// enable_external_firm_and_modules -> nötig, damit Luma die Patches in /luma/sysmodules lädt
|
||||||
|
// enable_game_patching -> nötig, damit Luma Patches in /luma/titles lädt (Miiverse)
|
||||||
|
// Beim Zurücksetzen wird eine Option nur wieder ausgeschaltet, wenn MBMNET sie eingeschaltet hat
|
||||||
|
// (Merker in /3ds/mbmnet/...).
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "lumaconfig.h"
|
||||||
|
|
||||||
|
#define CONFIG_FILE "sdmc:/luma/config.ini"
|
||||||
|
#define SPLASH_MARKER "sdmc:/3ds/mbmnet/splash-war-aus"
|
||||||
|
#define MODULES_MARKER "sdmc:/3ds/mbmnet/module-waren-aus"
|
||||||
|
#define PATCHING_MARKER "sdmc:/3ds/mbmnet/game-patching-war-aus"
|
||||||
|
|
||||||
|
// Setzt "key = value". Gibt den alten Wert in oldValue zurück. false, wenn Datei/Option fehlt.
|
||||||
|
static bool set_option(const char *key, const char *value, char *oldValue, size_t oldLen)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(CONFIG_FILE, "rb");
|
||||||
|
if (!f) return false;
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long size = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
if (size <= 0 || size > 64 * 1024) { fclose(f); return false; }
|
||||||
|
char *in = malloc(size + 1);
|
||||||
|
char *out = malloc(size + 128);
|
||||||
|
bool ok = in && out && fread(in, 1, size, f) == (size_t)size;
|
||||||
|
fclose(f);
|
||||||
|
if (!ok) { free(in); free(out); return false; }
|
||||||
|
in[size] = 0;
|
||||||
|
|
||||||
|
size_t keyLen = strlen(key);
|
||||||
|
bool found = false;
|
||||||
|
size_t o = 0;
|
||||||
|
for (char *line = in; *line;) {
|
||||||
|
char *end = strchr(line, '\n');
|
||||||
|
size_t len = end ? (size_t)(end - line + 1) : strlen(line);
|
||||||
|
char *p = line;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
char *eq = strchr(p, '=');
|
||||||
|
bool isKey = !found && strncmp(p, key, keyLen) == 0 && (p[keyLen] == ' ' || p[keyLen] == '=')
|
||||||
|
&& eq && eq < line + len;
|
||||||
|
if (isKey) {
|
||||||
|
char *v = eq + 1;
|
||||||
|
while (*v == ' ') v++;
|
||||||
|
size_t vl = strcspn(v, "\r\n");
|
||||||
|
snprintf(oldValue, oldLen, "%.*s", (int)vl, v);
|
||||||
|
o += sprintf(out + o, "%s = %s%s", key, value, (end && end > line && end[-1] == '\r') ? "\r\n" : "\n");
|
||||||
|
found = true;
|
||||||
|
} else {
|
||||||
|
memcpy(out + o, line, len);
|
||||||
|
o += len;
|
||||||
|
}
|
||||||
|
line += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found && strcasecmp(oldValue, value) != 0) {
|
||||||
|
f = fopen(CONFIG_FILE, "wb");
|
||||||
|
ok = f && fwrite(out, 1, o, f) == o;
|
||||||
|
if (f) fclose(f);
|
||||||
|
}
|
||||||
|
free(in);
|
||||||
|
free(out);
|
||||||
|
return found && ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void touch(const char *path)
|
||||||
|
{
|
||||||
|
FILE *m = fopen(path, "w");
|
||||||
|
if (m) fclose(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool exists(const char *path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
return stat(path, &st) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool lumaconfig_enable_splash(void)
|
||||||
|
{
|
||||||
|
char old[32] = "";
|
||||||
|
if (!set_option("splash_position", "after payloads", old, sizeof(old))) return false;
|
||||||
|
if (strcasecmp(old, "off") == 0) touch(SPLASH_MARKER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lumaconfig_restore_splash(void)
|
||||||
|
{
|
||||||
|
if (!exists(SPLASH_MARKER)) return; // war schon vorher an: nichts ändern
|
||||||
|
char old[32] = "";
|
||||||
|
set_option("splash_position", "off", old, sizeof(old));
|
||||||
|
remove(SPLASH_MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool lumaconfig_enable_modules(void)
|
||||||
|
{
|
||||||
|
char old[8] = "";
|
||||||
|
if (!set_option("enable_external_firm_and_modules", "1", old, sizeof(old))) return false;
|
||||||
|
if (strcmp(old, "0") == 0) touch(MODULES_MARKER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lumaconfig_keep_modules(void)
|
||||||
|
{
|
||||||
|
remove(MODULES_MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void lumaconfig_restore_modules(void)
|
||||||
|
{
|
||||||
|
if (!exists(MODULES_MARKER)) return;
|
||||||
|
char old[8] = "";
|
||||||
|
set_option("enable_external_firm_and_modules", "0", old, sizeof(old));
|
||||||
|
remove(MODULES_MARKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool lumaconfig_enable_game_patching(void)
|
||||||
|
{
|
||||||
|
char old[8] = "";
|
||||||
|
if (!set_option("enable_game_patching", "1", old, sizeof(old))) return false;
|
||||||
|
if (strcmp(old, "0") != 0) return false; // war schon an
|
||||||
|
touch(PATCHING_MARKER);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void lumaconfig_restore_game_patching(void)
|
||||||
|
{
|
||||||
|
if (!exists(PATCHING_MARKER)) return;
|
||||||
|
char old[8] = "";
|
||||||
|
set_option("enable_game_patching", "0", old, sizeof(old));
|
||||||
|
remove(PATCHING_MARKER);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
// Splash in Lumas Konfiguration einschalten ("after payloads"). false, wenn config.ini fehlt.
|
||||||
|
bool lumaconfig_enable_splash(void);
|
||||||
|
// Splash wieder ausschalten, falls MBMNET ihn eingeschaltet hat.
|
||||||
|
void lumaconfig_restore_splash(void);
|
||||||
|
|
||||||
|
// "Enable loading external FIRMs and system modules" einschalten - ohne diese Option lädt
|
||||||
|
// Luma die Patches in /luma/sysmodules nicht.
|
||||||
|
bool lumaconfig_enable_modules(void);
|
||||||
|
// Wieder ausschalten, falls MBMNET sie eingeschaltet hat.
|
||||||
|
void lumaconfig_restore_modules(void);
|
||||||
|
// Option dauerhaft anlassen (das FTP-Modul braucht sie auch im Nintendo-Modus)
|
||||||
|
void lumaconfig_keep_modules(void);
|
||||||
|
|
||||||
|
// "Enable game patching" einschalten - ohne sie lädt Luma die Patches in /luma/titles (Miiverse) nicht.
|
||||||
|
// true, wenn sie dadurch erst eingeschaltet wurde (dann ist ein Neustart nötig).
|
||||||
|
bool lumaconfig_enable_game_patching(void);
|
||||||
|
// Wieder ausschalten, falls MBMNET sie eingeschaltet hat.
|
||||||
|
void lumaconfig_restore_game_patching(void);
|
||||||
+547
@@ -0,0 +1,547 @@
|
|||||||
|
// MBMNET Switcher - schaltet den 3DS zwischen MBMNET und Nintendo (Original) um.
|
||||||
|
//
|
||||||
|
// Unten zwei Kacheln: MBMNET und Nintendo. Antippen schaltet um, unter der aktiven steht "Aktiv".
|
||||||
|
// SELECT blendet die Entwickler-Werkzeuge ein und aus (beim Öffnen immer aus).
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <citro2d.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "patcher.h"
|
||||||
|
#include "update.h"
|
||||||
|
#include "relaunch.h"
|
||||||
|
#include "nnid.h"
|
||||||
|
#include "frdswitch.h"
|
||||||
|
#include "admin.h"
|
||||||
|
#include "ftpctl.h"
|
||||||
|
#include "spotpass.h"
|
||||||
|
|
||||||
|
#define TOP_W 400
|
||||||
|
#define BOT_W 320
|
||||||
|
#define SCR_H 240
|
||||||
|
|
||||||
|
// Farben (gleiche Palette wie Logo und Webseite)
|
||||||
|
#define COL_RED C2D_Color32(0xE4, 0x00, 0x2B, 0xFF)
|
||||||
|
#define COL_RED_DARK C2D_Color32(0x9E, 0x00, 0x1E, 0xFF)
|
||||||
|
#define COL_BG C2D_Color32(0x16, 0x19, 0x1F, 0xFF)
|
||||||
|
#define COL_BG2 C2D_Color32(0x22, 0x26, 0x2E, 0xFF)
|
||||||
|
#define COL_CARD C2D_Color32(0x2A, 0x2F, 0x39, 0xFF)
|
||||||
|
#define COL_CARD_HI C2D_Color32(0x3A, 0x41, 0x4E, 0xFF)
|
||||||
|
#define COL_WHITE C2D_Color32(0xFF, 0xFF, 0xFF, 0xFF)
|
||||||
|
#define COL_MUTED C2D_Color32(0x9A, 0xA3, 0xAF, 0xFF)
|
||||||
|
#define COL_GREEN C2D_Color32(0x22, 0xC5, 0x5E, 0xFF)
|
||||||
|
#define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF)
|
||||||
|
#define COL_NINTENDO C2D_Color32(0xE6, 0x00, 0x12, 0xFF)
|
||||||
|
|
||||||
|
// Bedienelemente auf dem unteren Bildschirm. Die Werkzeuge gibt es nur für Admins.
|
||||||
|
enum { BTN_ENABLE, BTN_DISABLE, BTN_DIAG, BTN_FTP, BTN_UNLINK, BTN_SPOTPASS, BTN_REBOOT, BTN_COUNT };
|
||||||
|
|
||||||
|
typedef struct { float x, y, w, h; } Box;
|
||||||
|
|
||||||
|
#define TILE_Y 14
|
||||||
|
#define TILE_W 142
|
||||||
|
#define TILE_H_FULL 210
|
||||||
|
#define TILE_H_ADMIN 160
|
||||||
|
#define TOOL_COUNT 5
|
||||||
|
static const Box toolBoxes[TOOL_COUNT] = { { 12, 184, 56, 42 }, { 72, 184, 56, 42 }, { 132, 184, 56, 42 }, { 192, 184, 56, 42 }, { 252, 184, 56, 42 } };
|
||||||
|
|
||||||
|
static Box box_of(int btn, bool admin)
|
||||||
|
{
|
||||||
|
float h = admin ? TILE_H_ADMIN : TILE_H_FULL;
|
||||||
|
switch (btn) {
|
||||||
|
case BTN_ENABLE: return (Box){ 12, TILE_Y, TILE_W, h };
|
||||||
|
case BTN_DISABLE: return (Box){ 166, TILE_Y, TILE_W, h };
|
||||||
|
case BTN_DIAG: return toolBoxes[0];
|
||||||
|
case BTN_FTP: return toolBoxes[1];
|
||||||
|
case BTN_UNLINK: return toolBoxes[2];
|
||||||
|
case BTN_SPOTPASS: return toolBoxes[3];
|
||||||
|
default: return toolBoxes[4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static C2D_TextBuf textBuf;
|
||||||
|
static int spotpassTestFrames = -1; // läuft der SpotPass-Test? (Bilder seit dem Start)
|
||||||
|
static bool ftpOn; // läuft das FTP-Modul? (alle 2 s geprüft, nicht in jedem Bild)
|
||||||
|
|
||||||
|
// --- Zeichenhilfen ------------------------------------------------------------
|
||||||
|
|
||||||
|
static void text(const char *s, float x, float y, float size, u32 color, u32 flags)
|
||||||
|
{
|
||||||
|
C2D_Text t;
|
||||||
|
C2D_TextParse(&t, textBuf, s);
|
||||||
|
C2D_TextOptimize(&t);
|
||||||
|
C2D_DrawText(&t, C2D_WithColor | flags, x, y, 0.5f, size, size, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void round_rect(float x, float y, float w, float h, float r, u32 color)
|
||||||
|
{
|
||||||
|
C2D_DrawRectSolid(x + r, y, 0.5f, w - 2 * r, h, color);
|
||||||
|
C2D_DrawRectSolid(x, y + r, 0.5f, w, h - 2 * r, color);
|
||||||
|
C2D_DrawCircleSolid(x + r, y + r, 0.5f, r, color);
|
||||||
|
C2D_DrawCircleSolid(x + w - r, y + r, 0.5f, r, color);
|
||||||
|
C2D_DrawCircleSolid(x + r, y + h - r, 0.5f, r, color);
|
||||||
|
C2D_DrawCircleSolid(x + w - r, y + h - r, 0.5f, r, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Das MBMNET-Logo: rotes abgerundetes Quadrat mit einem "M" aus verbundenen Netzwerk-Knoten.
|
||||||
|
// Geometrie identisch zu assets/logo.svg (100x100-Raster).
|
||||||
|
static void draw_logo(float x, float y, float size)
|
||||||
|
{
|
||||||
|
float s = size / 100.0f;
|
||||||
|
round_rect(x, y, size, size, 22 * s, COL_RED_DARK);
|
||||||
|
round_rect(x, y, size, size - 4 * s, 22 * s, COL_RED);
|
||||||
|
|
||||||
|
static const float pts[5][2] = { {24, 72}, {24, 30}, {50, 56}, {76, 30}, {76, 72} };
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
C2D_DrawLine(x + pts[i][0] * s, y + pts[i][1] * s, COL_WHITE,
|
||||||
|
x + pts[i + 1][0] * s, y + pts[i + 1][1] * s, COL_WHITE, 7 * s, 0.5f);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
C2D_DrawCircleSolid(x + pts[i][0] * s, y + pts[i][1] * s, 0.5f, 9 * s, COL_WHITE);
|
||||||
|
if (i == 2) C2D_DrawCircleSolid(x + pts[i][0] * s, y + pts[i][1] * s, 0.5f, 4 * s, COL_RED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nintendo-Schriftzug im roten Rahmen (nachgezeichnet, kein Bild)
|
||||||
|
static void draw_nintendo(float cx, float cy, float w)
|
||||||
|
{
|
||||||
|
float h = w * 0.36f, r = h / 2;
|
||||||
|
round_rect(cx - w / 2, cy - h / 2, w, h, r, COL_NINTENDO);
|
||||||
|
round_rect(cx - w / 2 + 3, cy - h / 2 + 3, w - 6, h - 6, r - 3, COL_WHITE);
|
||||||
|
text("Nintendo", cx, cy - h * 0.36f, w / 150.0f, COL_NINTENDO, C2D_AlignCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bildschirme ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void draw_update_line(bool admin)
|
||||||
|
{
|
||||||
|
char line[64];
|
||||||
|
const char *v = update_new_version();
|
||||||
|
switch (update_state()) {
|
||||||
|
case UPD_CHECKING: snprintf(line, sizeof(line), "v" APP_VERSION); break;
|
||||||
|
case UPD_AVAILABLE: snprintf(line, sizeof(line), "Update auf v%s bereit", v); break;
|
||||||
|
case UPD_DOWNLOADING:
|
||||||
|
if (update_attempt() > 1) snprintf(line, sizeof(line), "lade v%s (Versuch %d) ...", v, update_attempt());
|
||||||
|
else snprintf(line, sizeof(line), "lade v%s ...", v);
|
||||||
|
break;
|
||||||
|
case UPD_INSTALLED: snprintf(line, sizeof(line), "v%s installiert", v); break;
|
||||||
|
case UPD_ERROR: snprintf(line, sizeof(line), admin ? "Update-Server offline" : "v" APP_VERSION); break;
|
||||||
|
default: snprintf(line, sizeof(line), "v" APP_VERSION); break;
|
||||||
|
}
|
||||||
|
u32 color = update_state() == UPD_INSTALLED ? COL_GREEN : update_pending() ? COL_AMBER : COL_MUTED;
|
||||||
|
text(line, TOP_W - 10, 4, 0.4f, color, C2D_AlignRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_top(PatchState st, const char *msg, bool admin, bool isAdmin)
|
||||||
|
{
|
||||||
|
C2D_DrawRectangle(0, 0, 0, TOP_W, SCR_H, COL_BG2, COL_BG2, COL_BG, COL_BG);
|
||||||
|
draw_update_line(admin);
|
||||||
|
const char *hint = admin ? "START: Beenden SELECT: Werkzeuge aus" : "START: Beenden SELECT: Werkzeuge";
|
||||||
|
text(hint, 10, 4, 0.4f, COL_MUTED, 0);
|
||||||
|
if (ftpOn) {
|
||||||
|
// FTP-Adresse direkt daneben
|
||||||
|
C2D_Text t;
|
||||||
|
float w = 0;
|
||||||
|
C2D_TextParse(&t, textBuf, hint);
|
||||||
|
C2D_TextGetDimensions(&t, 0.4f, 0.4f, &w, NULL);
|
||||||
|
char ftp[112];
|
||||||
|
const char *a = ftpctl_address();
|
||||||
|
const char *st = ftpctl_status();
|
||||||
|
if (a[0]) snprintf(ftp, sizeof(ftp), "FTP %s", a);
|
||||||
|
else snprintf(ftp, sizeof(ftp), "FTP: %s", st[0] ? st : "startet ...");
|
||||||
|
text(ftp, 10 + w + 12, 4, 0.4f, COL_GREEN, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float y = msg[0] || admin ? 26 : 62;
|
||||||
|
draw_logo(TOP_W / 2 - 32, y, 64);
|
||||||
|
text("MBMNET", TOP_W / 2, y + 70, 0.9f, COL_WHITE, C2D_AlignCenter);
|
||||||
|
y += 108;
|
||||||
|
|
||||||
|
// Admins: was gerade wirklich los ist
|
||||||
|
if (admin) {
|
||||||
|
char line[96], id[20];
|
||||||
|
const char *label = st == STATE_MBMNET ? "Patch: MBMNET" : st == STATE_FOREIGN ? "Patch: fremd (z. B. Pretendo)" : "Patch: keiner";
|
||||||
|
snprintf(line, sizeof(line), "%s · NNID: %s%s", label, nnid_linked(id, sizeof(id)) ? id : "keine", isAdmin ? " · Admin" : "");
|
||||||
|
text(line, TOP_W / 2, y, 0.42f, COL_MUTED, C2D_AlignCenter);
|
||||||
|
y += 18;
|
||||||
|
}
|
||||||
|
if (msg[0]) {
|
||||||
|
round_rect(16, y, TOP_W - 32, SCR_H - y - 8, 10, COL_CARD);
|
||||||
|
text(msg, 28, y + 6, 0.46f, COL_WHITE, 0);
|
||||||
|
} else if (!admin) {
|
||||||
|
text("Tippe unten auf MBMNET oder Nintendo.", TOP_W / 2, 200, 0.45f, COL_MUTED, C2D_AlignCenter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_tile(int btn, bool sel, bool active, bool admin)
|
||||||
|
{
|
||||||
|
Box b = box_of(btn, admin);
|
||||||
|
if (sel) round_rect(b.x - 3, b.y - 3, b.w + 6, b.h + 6, 15, COL_RED);
|
||||||
|
round_rect(b.x, b.y, b.w, b.h, 12, active ? COL_CARD_HI : COL_CARD);
|
||||||
|
|
||||||
|
float cx = b.x + b.w / 2, logoCy = b.y + b.h / 2 - 12;
|
||||||
|
if (btn == BTN_ENABLE) draw_logo(cx - 36, logoCy - 36, 72);
|
||||||
|
else draw_nintendo(cx, logoCy, 116);
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
C2D_DrawCircleSolid(cx - 26, b.y + b.h - 26, 0.5f, 5, COL_GREEN);
|
||||||
|
text("Aktiv", cx + 6, b.y + b.h - 36, 0.6f, COL_GREEN, C2D_AlignCenter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw_bottom(int selected, PatchState st, bool admin)
|
||||||
|
{
|
||||||
|
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
|
||||||
|
draw_tile(BTN_ENABLE, selected == BTN_ENABLE, st == STATE_MBMNET, admin);
|
||||||
|
draw_tile(BTN_DISABLE, selected == BTN_DISABLE, st == STATE_ORIGINAL, admin);
|
||||||
|
if (!admin) return;
|
||||||
|
|
||||||
|
const char *labels[TOOL_COUNT] = { "Diagnose", ftpOn ? "FTP aus" : "FTP an", "ID lösen", "SpotPass", "Neustart" };
|
||||||
|
for (int i = 0; i < TOOL_COUNT; i++) {
|
||||||
|
Box b = toolBoxes[i];
|
||||||
|
bool sel = selected == BTN_DIAG + i;
|
||||||
|
bool on = i == 1 && ftpOn;
|
||||||
|
round_rect(b.x, b.y, b.w, b.h, 10, sel ? COL_CARD_HI : COL_CARD);
|
||||||
|
if (sel) C2D_DrawRectSolid(b.x, b.y + 10, 0.5f, 4, b.h - 20, COL_RED);
|
||||||
|
if (on) C2D_DrawCircleSolid(b.x + 12, b.y + b.h / 2, 0.5f, 4, COL_GREEN);
|
||||||
|
text(labels[i], b.x + b.w / 2, b.y + 13, 0.45f, on ? COL_GREEN : COL_WHITE, C2D_AlignCenter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Aktionen --------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Wie bei Pretendo hat MBMNET ein eigenes Freundekonto (Nr. 3) mit eigenem Konto-Platz. Umschalten =
|
||||||
|
// Patches an/aus + auf das andere Freundekonto wechseln + Neustart. Beide Anmeldungen bleiben dabei
|
||||||
|
// erhalten, die Nintendo-Freundesliste auch.
|
||||||
|
|
||||||
|
// Merker älterer Versionen (bis v2.4): Die MBMNET-ID wurde damals im Nintendo-Platz verknüpft
|
||||||
|
#define UNLINKED_MARKER "sdmc:/3ds/mbmnet/nnid-geloest"
|
||||||
|
static bool old_mbmnet_link(void) { FILE *f = fopen(UNLINKED_MARKER, "r"); if (f) fclose(f); return f != NULL; }
|
||||||
|
|
||||||
|
static int armed = -1; // Sicherheitsabfrage (zweimal antippen)
|
||||||
|
|
||||||
|
// Name der MBMNET-ID, gemerkt beim Wechsel zu Nintendo. Der 3DS erlaubt dieselbe ID nicht in zwei Konto-Plätzen
|
||||||
|
// (022-2571): Heißt die Pretendo-ID genauso, muss sie beim Wechsel zurück zu MBMNET dort gelöst werden.
|
||||||
|
#define MBMNET_ID_FILE "sdmc:/3ds/mbmnet/mbmnet-id.txt"
|
||||||
|
static void remember_mbmnet_id(const char *id)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(MBMNET_ID_FILE, "w");
|
||||||
|
if (f) { fputs(id, f); fclose(f); }
|
||||||
|
}
|
||||||
|
static void remembered_mbmnet_id(char *out, size_t len)
|
||||||
|
{
|
||||||
|
out[0] = 0;
|
||||||
|
FILE *f = fopen(MBMNET_ID_FILE, "r");
|
||||||
|
if (!f) return;
|
||||||
|
if (fgets(out, len, f)) out[strcspn(out, "\r\n")] = 0;
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Umschalten auf MBMNET. true = der 3DS muss jetzt neu starten.
|
||||||
|
static bool enable(bool confirmed, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
char detail[160] = "", id[20];
|
||||||
|
if (patch_state() == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET) {
|
||||||
|
// Freundekonto mit fremder Kennung (z. B. von Pretendo geholt): neu anlegen, dann holt es sich
|
||||||
|
// beim nächsten Start eine Kennung bei MBMNET
|
||||||
|
u32 pid = frdswitch_my_pid();
|
||||||
|
if (pid && !frdswitch_pid_is_mbmnet(pid)) {
|
||||||
|
if (!confirmed) {
|
||||||
|
armed = BTN_ENABLE;
|
||||||
|
snprintf(msg, msglen, "Dein MBMNET-Freundekonto hat eine fremde Kennung\n(%lu, nicht von MBMNET). Neu anlegen?\n"
|
||||||
|
"Die Verknüpfung mit deinem MBMNET-Konto bleibt.\nZum Bestätigen nochmal antippen.", (unsigned long)pid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!frdswitch_recreate_mbmnet(msg, msglen)) return false;
|
||||||
|
snprintf(msg, msglen, "MBMNET-Freundekonto neu angelegt. Der 3DS startet neu.\n"
|
||||||
|
"Danach holt es sich bei MBMNET deinen Freundescode.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool linked = frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id));
|
||||||
|
snprintf(msg, msglen, linked ? "MBMNET ist schon aktiv (Konto %s)." : "MBMNET ist schon aktiv.\n"
|
||||||
|
"Jetzt dein MBMNET-Konto verknüpfen:\nEinstellungen > Nintendo Network ID", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Umstieg von älteren Versionen: Dort steckte die MBMNET-ID im Nintendo-Platz. Sie muss dort raus,
|
||||||
|
// sonst lässt sie sich im eigenen MBMNET-Platz nicht verknüpfen (Fehler 022-2571).
|
||||||
|
bool migrate = old_mbmnet_link() && frdswitch_slot_linked(FRIEND_ACCOUNT_NINTENDO, id, sizeof(id));
|
||||||
|
// Die gemerkte MBMNET-ID steckt noch in einem anderen Platz (z. B. Pretendo mit gleichem Namen)?
|
||||||
|
char mbmnetId[20];
|
||||||
|
remembered_mbmnet_id(mbmnetId, sizeof(mbmnetId));
|
||||||
|
u8 mbmnetSlot = frdswitch_slot_of(FRIEND_ACCOUNT_MBMNET);
|
||||||
|
bool sameNameElsewhere = !migrate && mbmnetId[0] && nnid_name_elsewhere(mbmnetId, mbmnetSlot);
|
||||||
|
if (sameNameElsewhere && !confirmed) {
|
||||||
|
armed = BTN_ENABLE;
|
||||||
|
snprintf(msg, msglen, "\"%s\" ist noch in einem anderen Platz verknüpft (z. B. Pretendo).\n"
|
||||||
|
"Der 3DS erlaubt sie nur einmal: Sie wird dort gelöst, damit du sie\nbei MBMNET verknüpfen kannst. Zum Bestätigen nochmal antippen.", mbmnetId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (migrate && !confirmed) {
|
||||||
|
armed = BTN_ENABLE;
|
||||||
|
snprintf(msg, msglen, "Umstellung auf ein eigenes MBMNET-Freundekonto:\n\"%s\" wird aus dem Nintendo-Platz gelöst\n"
|
||||||
|
"und danach im MBMNET-Platz neu verknüpft.\nZum Bestätigen nochmal antippen.", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
if (!patch_enable(MBMNET_SERVER, &changed, detail, sizeof(detail))) {
|
||||||
|
snprintf(msg, msglen, "%s", detail);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (migrate && !nnid_unlink(msg, msglen)) return false;
|
||||||
|
remove(UNLINKED_MARKER);
|
||||||
|
if (sameNameElsewhere && !nnid_unlink_named(mbmnetId, mbmnetSlot, msg, msglen)) {
|
||||||
|
char ignore[160];
|
||||||
|
patch_disable(ignore, sizeof(ignore));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool created = false;
|
||||||
|
if (!frdswitch_to(FRIEND_ACCOUNT_MBMNET, &created, msg, msglen)) {
|
||||||
|
char ignore[160];
|
||||||
|
patch_disable(ignore, sizeof(ignore)); // nichts halb umgestellt lassen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool linked = frdswitch_slot_linked(FRIEND_ACCOUNT_MBMNET, id, sizeof(id));
|
||||||
|
char ignore[96];
|
||||||
|
spotpass_register(MBMNET_SERVER, ignore, sizeof(ignore)); // Mitteilungen per SpotPass
|
||||||
|
snprintf(msg, msglen, "MBMNET aktiv! Der 3DS startet neu.\n%s",
|
||||||
|
created ? "Eigenes MBMNET-Freundekonto angelegt. Danach einmal:\nEinstellungen > Nintendo Network ID > Vorhandene ID"
|
||||||
|
: linked ? "Deine MBMNET-Anmeldung und -Freundesliste sind wieder da."
|
||||||
|
: "Danach einmal: Einstellungen > Nintendo Network ID");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zurück zu Nintendo. true = der 3DS muss jetzt neu starten.
|
||||||
|
static bool disable(bool confirmed, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
(void)confirmed;
|
||||||
|
char detail[160] = "";
|
||||||
|
PatchState st = patch_state();
|
||||||
|
if (st == STATE_ORIGINAL && frdswitch_current() == FRIEND_ACCOUNT_NINTENDO) {
|
||||||
|
snprintf(msg, msglen, "Nintendo ist schon aktiv.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (st == STATE_FOREIGN) {
|
||||||
|
snprintf(msg, msglen, "MBMNET ist nicht aktiv (anderer Patch).");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// MBMNET-ID aus dem MBMNET-Platz lösen und merken: Heißt die Pretendo-ID genauso, ließe sie sich sonst
|
||||||
|
// im Pretendo-Platz nicht verknüpfen (022-2571). Beim Wechsel zurück zu MBMNET neu anmelden.
|
||||||
|
char id[20] = "";
|
||||||
|
bool unlinked = false;
|
||||||
|
if (frdswitch_current() == FRIEND_ACCOUNT_MBMNET && nnid_linked(id, sizeof(id))) {
|
||||||
|
remember_mbmnet_id(id);
|
||||||
|
if (!nnid_unlink(msg, msglen)) return false;
|
||||||
|
unlinked = true;
|
||||||
|
}
|
||||||
|
bool created = false;
|
||||||
|
if (frdswitch_current() != FRIEND_ACCOUNT_NINTENDO && !frdswitch_to(FRIEND_ACCOUNT_NINTENDO, &created, msg, msglen)) return false;
|
||||||
|
patch_disable(detail, sizeof(detail));
|
||||||
|
spotpass_unregister();
|
||||||
|
if (unlinked)
|
||||||
|
snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\n\"%s\" wurde vom MBMNET-Platz gelöst (für Pretendo).\n"
|
||||||
|
"Zurück zu MBMNET: danach einmal neu anmelden.", id);
|
||||||
|
else
|
||||||
|
snprintf(msg, msglen, "Nintendo aktiv! Der 3DS startet neu.\nDeine Nintendo-Freundesliste ist wieder da.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Führt eine Aktion aus. true = der 3DS muss jetzt neu starten.
|
||||||
|
static bool run(int action, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
bool confirmed = armed == action;
|
||||||
|
armed = -1;
|
||||||
|
// Während ein Update lädt oder installiert wird, nicht umschalten (der 3DS würde mittendrin neu starten)
|
||||||
|
if (action != BTN_DIAG && action != BTN_FTP && action != BTN_SPOTPASS && update_pending()) {
|
||||||
|
snprintf(msg, msglen, "Moment: Das Update auf v%s wird gerade installiert.\nDanach startet die App neu.", update_new_version());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (action) {
|
||||||
|
case BTN_ENABLE: return enable(confirmed, msg, msglen);
|
||||||
|
case BTN_DISABLE: return disable(confirmed, msg, msglen);
|
||||||
|
case BTN_DIAG: patch_diagnose(msg, msglen); return false;
|
||||||
|
case BTN_UNLINK: {
|
||||||
|
// Verknüpfte NNID nur auf dem 3DS lösen (ohne Server) - z. B. wenn das Konto gelöscht wurde
|
||||||
|
char id[20];
|
||||||
|
if (!nnid_linked(id, sizeof(id))) { snprintf(msg, msglen, "Hier ist keine ID verknüpft."); return false; }
|
||||||
|
if (!confirmed) {
|
||||||
|
armed = BTN_UNLINK;
|
||||||
|
snprintf(msg, msglen, "\"%s\" vom 3DS lösen?\nNur auf dem 3DS, das Konto selbst bleibt unberührt.\n"
|
||||||
|
"Zum Bestätigen nochmal antippen.", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!nnid_unlink(msg, msglen)) return false;
|
||||||
|
snprintf(msg, msglen, "\"%s\" gelöst. Der 3DS startet neu.\nDanach kannst du eine andere ID verknüpfen.", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case BTN_FTP:
|
||||||
|
if (ftpctl_running()) ftpctl_stop(msg, msglen);
|
||||||
|
else ftpctl_start(msg, msglen);
|
||||||
|
ftpOn = ftpctl_running();
|
||||||
|
return false;
|
||||||
|
case BTN_SPOTPASS:
|
||||||
|
if (spotpassTestFrames >= 0) { snprintf(msg, msglen, "SpotPass-Test läuft schon ..."); return false; }
|
||||||
|
if (spotpass_test_start(MBMNET_SERVER, msg, msglen)) spotpassTestFrames = 0;
|
||||||
|
return false;
|
||||||
|
case BTN_REBOOT: return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool inside(Box b, touchPosition tp) { return tp.px >= b.x && tp.px < b.x + b.w && tp.py >= b.y && tp.py < b.y + b.h; }
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
gfxInitDefault();
|
||||||
|
httpcInit(0);
|
||||||
|
// Als installierte CIA: am:net zum Installieren von Updates
|
||||||
|
bool am = !envIsHomebrew() && R_SUCCEEDED(amInit());
|
||||||
|
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
|
||||||
|
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
|
||||||
|
C2D_Prepare();
|
||||||
|
C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
|
||||||
|
C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
|
||||||
|
textBuf = C2D_TextBufNew(4096);
|
||||||
|
nnid_init();
|
||||||
|
frdswitch_init();
|
||||||
|
|
||||||
|
const char *server = MBMNET_SERVER;
|
||||||
|
char msg[256] = "";
|
||||||
|
|
||||||
|
// Pfad der laufenden App (vom Homebrew Launcher übergeben), damit das Update sie ersetzen kann
|
||||||
|
const char *selfPath = argc > 0 && strncmp(argv[0], "sdmc:/", 6) == 0 ? argv[0] : "sdmc:/3ds/mbmnet.3dsx";
|
||||||
|
update_start(server, selfPath);
|
||||||
|
UpdateState lastUpd = UPD_IDLE;
|
||||||
|
int restartIn = 0; // Frames bis zum automatischen Neustart der App nach einem Update
|
||||||
|
int rebootIn = 0; // Frames bis zum Neustart des 3DS nach dem Umschalten
|
||||||
|
|
||||||
|
// Patches alter Versionen lagen an einer Stelle, an der Luma sie nicht lädt
|
||||||
|
if (patch_migrate())
|
||||||
|
snprintf(msg, sizeof(msg), "Neuer Speicherort für die Patches!\nBitte MBMNET einmal neu antippen.");
|
||||||
|
PatchState st = patch_state();
|
||||||
|
// Nur im MBMNET-Modus ist die verknüpfte NNID ein MBMNET-Konto
|
||||||
|
admin_start(server, st == STATE_MBMNET ? nnid_pid() : 0);
|
||||||
|
|
||||||
|
// MBMNET aktiv, aber das Freundekonto hat eine fremde Kennung: darauf hinweisen
|
||||||
|
if (st == STATE_MBMNET && frdswitch_current() == FRIEND_ACCOUNT_MBMNET) {
|
||||||
|
u32 pid = frdswitch_my_pid();
|
||||||
|
if (pid && !frdswitch_pid_is_mbmnet(pid))
|
||||||
|
snprintf(msg, sizeof(msg), "Dein MBMNET-Freundekonto hat eine fremde Kennung (%lu).\n"
|
||||||
|
"Tippe auf MBMNET, um es neu anzulegen.", (unsigned long)pid);
|
||||||
|
}
|
||||||
|
// Mitteilungen per SpotPass: bei aktivem MBMNET die Hintergrund-Aufgabe (neu) anmelden
|
||||||
|
if (st == STATE_MBMNET) {
|
||||||
|
char ignore[96];
|
||||||
|
spotpass_register(server, ignore, sizeof(ignore));
|
||||||
|
// Patches auffrischen: neue Versionen bringen neue Ziele mit (z. B. Miiverse ab v2.8.0)
|
||||||
|
bool changed = false;
|
||||||
|
char detail[160];
|
||||||
|
if (patch_enable(server, &changed, detail, sizeof(detail)) && changed && !msg[0])
|
||||||
|
snprintf(msg, sizeof(msg), "Neue MBMNET-Patches (Miiverse) geschrieben.\n"
|
||||||
|
"Bitte den 3DS einmal neu starten (Werkzeuge: Neustart).");
|
||||||
|
}
|
||||||
|
// Das Hintergrund-Modul ist abgeschaltet (Kernel-Absturz): Reste wegräumen, nie wieder starten
|
||||||
|
ftpctl_cleanup_module();
|
||||||
|
ftpOn = ftpctl_running();
|
||||||
|
int ftpCheckIn = 120;
|
||||||
|
|
||||||
|
int selected = st == STATE_MBMNET ? BTN_DISABLE : BTN_ENABLE;
|
||||||
|
bool toolsToggled = false;
|
||||||
|
int queued = -1; // Aktion, die nach dem nächsten Bild läuft (damit sofort etwas passiert)
|
||||||
|
|
||||||
|
while (aptMainLoop()) {
|
||||||
|
hidScanInput();
|
||||||
|
u32 down = hidKeysDown();
|
||||||
|
if (--ftpCheckIn <= 0) { ftpCheckIn = 120; ftpOn = ftpctl_running(); }
|
||||||
|
if (down & KEY_START && rebootIn == 0) break; // während des Umschaltens nicht abbrechen
|
||||||
|
if (down & KEY_SELECT) toolsToggled = !toolsToggled;
|
||||||
|
// Werkzeuge nur mit SELECT, beim Öffnen immer aus
|
||||||
|
bool admin = toolsToggled;
|
||||||
|
if (!admin && selected >= BTN_DIAG) selected = BTN_ENABLE;
|
||||||
|
|
||||||
|
int action = -1;
|
||||||
|
// Oben die beiden Kacheln, unten (nur mit Werkzeugen) Diagnose / FTP / ID lösen / SpotPass / Neustart
|
||||||
|
bool left = down & (KEY_LEFT | KEY_CPAD_LEFT), right = down & (KEY_RIGHT | KEY_CPAD_RIGHT);
|
||||||
|
if (selected < BTN_DIAG) { if (left || right) selected ^= 1; }
|
||||||
|
else if (left) selected = selected == BTN_DIAG ? BTN_REBOOT : selected - 1;
|
||||||
|
else if (right) selected = selected == BTN_REBOOT ? BTN_DIAG : selected + 1;
|
||||||
|
if (admin && down & (KEY_DOWN | KEY_CPAD_DOWN) && selected < BTN_DIAG) selected = selected == BTN_ENABLE ? BTN_DIAG : BTN_REBOOT;
|
||||||
|
if (down & (KEY_UP | KEY_CPAD_UP) && selected >= BTN_DIAG) selected = selected == BTN_REBOOT ? BTN_DISABLE : BTN_ENABLE;
|
||||||
|
if (down & KEY_A) action = selected;
|
||||||
|
if (down & KEY_TOUCH) {
|
||||||
|
touchPosition tp;
|
||||||
|
hidTouchRead(&tp);
|
||||||
|
for (int i = 0; i < (admin ? BTN_COUNT : BTN_DIAG); i++)
|
||||||
|
if (inside(box_of(i, admin), tp)) selected = action = i;
|
||||||
|
}
|
||||||
|
if (queued >= 0) {
|
||||||
|
int a = queued;
|
||||||
|
queued = -1;
|
||||||
|
if (run(a, msg, sizeof(msg))) rebootIn = 45; // ca. 0,75 Sekunden zum Lesen
|
||||||
|
st = patch_state();
|
||||||
|
} else if (action >= 0 && rebootIn == 0) {
|
||||||
|
queued = action;
|
||||||
|
snprintf(msg, sizeof(msg), action <= BTN_DISABLE ? "Schalte um ..." : "Einen Moment ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update gefunden: gleich laden, egal ob MBMNET oder Nintendo aktiv ist. Nicht während des
|
||||||
|
// Umschaltens, sonst startet der 3DS mitten in der Installation neu.
|
||||||
|
UpdateState upd = update_state();
|
||||||
|
if (upd == UPD_AVAILABLE && rebootIn == 0 && queued < 0) {
|
||||||
|
update_download();
|
||||||
|
snprintf(msg, sizeof(msg), "Lade Update auf v%s ...", update_new_version());
|
||||||
|
upd = update_state();
|
||||||
|
}
|
||||||
|
// Update fertig: kurz anzeigen, dann neu starten
|
||||||
|
if (upd == UPD_INSTALLED && lastUpd != UPD_INSTALLED) {
|
||||||
|
snprintf(msg, sizeof(msg), "Update auf v%s installiert!\nDie App startet gleich neu ...", update_new_version());
|
||||||
|
restartIn = 90; // ca. 1,5 Sekunden
|
||||||
|
}
|
||||||
|
if (upd == UPD_ERROR && lastUpd == UPD_DOWNLOADING) {
|
||||||
|
char why[96];
|
||||||
|
update_error(why, sizeof(why));
|
||||||
|
snprintf(msg, sizeof(msg), "Update nach 3 Versuchen fehlgeschlagen:\n%s\nProtokoll: /3ds/mbmnet/update.log", why);
|
||||||
|
}
|
||||||
|
lastUpd = upd;
|
||||||
|
|
||||||
|
// SpotPass-Test: jede Sekunde nachsehen
|
||||||
|
if (spotpassTestFrames >= 0 && ++spotpassTestFrames % 60 == 0 &&
|
||||||
|
!spotpass_test_poll(spotpassTestFrames / 60, msg, sizeof(msg)))
|
||||||
|
spotpassTestFrames = -1;
|
||||||
|
|
||||||
|
// Neustart erst, wenn die Freunde-Anmeldung durch ist (höchstens ein paar Sekunden)
|
||||||
|
if (rebootIn > 0 && --rebootIn == 0 && R_FAILED(APT_HardwareResetAsync())) {
|
||||||
|
snprintf(msg, sizeof(msg), "Neustart fehlgeschlagen.\nBitte den 3DS von Hand neu starten.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restartIn > 0 && --restartIn == 0) {
|
||||||
|
if (relaunch_prepare(selfPath)) break;
|
||||||
|
snprintf(msg, sizeof(msg), "Update installiert!\nBitte die App von Hand neu starten (START).");
|
||||||
|
}
|
||||||
|
|
||||||
|
C2D_TextBufClear(textBuf);
|
||||||
|
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
|
||||||
|
C2D_TargetClear(top, COL_BG);
|
||||||
|
C2D_SceneBegin(top);
|
||||||
|
draw_top(st, msg, admin, admin_is());
|
||||||
|
C2D_TargetClear(bot, COL_BG);
|
||||||
|
C2D_SceneBegin(bot);
|
||||||
|
draw_bottom(selected, st, admin);
|
||||||
|
C3D_FrameEnd(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ftpctl_stop(NULL, 0);
|
||||||
|
admin_finish();
|
||||||
|
update_finish();
|
||||||
|
frdswitch_exit();
|
||||||
|
nnid_exit();
|
||||||
|
httpcExit();
|
||||||
|
if (am) amExit();
|
||||||
|
C2D_TextBufDelete(textBuf);
|
||||||
|
C2D_Fini();
|
||||||
|
C3D_Fini();
|
||||||
|
gfxExit();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+217
@@ -0,0 +1,217 @@
|
|||||||
|
// Nintendo Network ID auf dem 3DS: anzeigen und lokal lösen.
|
||||||
|
//
|
||||||
|
// Solange eine NNID verknüpft ist, bieten die Systemeinstellungen kein
|
||||||
|
// "Vorhandene ID verknüpfen" an. Damit man sich mit dem MBMNET-Konto anmelden kann,
|
||||||
|
// löst die App die Verknüpfung lokal (ohne Server). Name und PID der alten ID werden
|
||||||
|
// vorher in /3ds/mbmnet/alte-nnid.txt gesichert.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <strings.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include "nnid.h"
|
||||||
|
|
||||||
|
#define OLD_NNID_FILE "sdmc:/3ds/mbmnet/alte-nnid.txt"
|
||||||
|
#define LOG_FILE "sdmc:/3ds/mbmnet/konto.log"
|
||||||
|
|
||||||
|
static bool ready;
|
||||||
|
|
||||||
|
Result nnid_init(void)
|
||||||
|
{
|
||||||
|
Result rc = actInit(false); // false = act:a (Verwaltung)
|
||||||
|
ready = R_SUCCEEDED(rc);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void nnid_exit(void)
|
||||||
|
{
|
||||||
|
if (ready) actExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
static u8 current_slot(void)
|
||||||
|
{
|
||||||
|
u8 slot = 0;
|
||||||
|
ACT_GetCommonInfo(&slot, sizeof(slot), INFO_TYPE_COMMON_CURRENT_ACCOUNT_SLOT);
|
||||||
|
return slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nnid_linked(char *name, size_t len)
|
||||||
|
{
|
||||||
|
name[0] = 0;
|
||||||
|
if (!ready) return false;
|
||||||
|
AccountId id = { 0 };
|
||||||
|
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), current_slot(), INFO_TYPE_ACCOUNT_ID))) return false;
|
||||||
|
snprintf(name, len, "%s", id);
|
||||||
|
return name[0] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 nnid_pid(void)
|
||||||
|
{
|
||||||
|
u32 pid = 0;
|
||||||
|
char name[20];
|
||||||
|
if (nnid_linked(name, sizeof(name))) ACT_GetAccountInfo(&pid, sizeof(pid), current_slot(), INFO_TYPE_PRINCIPAL_ID);
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verknüpfte IDs in ANDEREN Konto-Plätzen (z. B. die Pretendo-ID aus Nimbus' zweitem Platz).
|
||||||
|
// Eine ID, die dort steht, darf der 3DS nicht noch einmal verknüpfen (Fehler 022-2571).
|
||||||
|
int nnid_others(char *names, size_t len)
|
||||||
|
{
|
||||||
|
names[0] = 0;
|
||||||
|
if (!ready) return 0;
|
||||||
|
u8 count = 0, current = current_slot();
|
||||||
|
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return 0;
|
||||||
|
int found = 0;
|
||||||
|
for (u8 slot = 1; slot <= count; slot++) {
|
||||||
|
if (slot == current) continue;
|
||||||
|
AccountId id = { 0 };
|
||||||
|
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) || !id[0]) continue;
|
||||||
|
size_t n = strlen(names);
|
||||||
|
snprintf(names + n, len - n, "%s\"%s\"", found ? ", " : "", id);
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nnid_unlink_others(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (!ready) return false;
|
||||||
|
u8 count = 0, current = current_slot();
|
||||||
|
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return false;
|
||||||
|
|
||||||
|
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||||
|
FILE *log = fopen(LOG_FILE, "a");
|
||||||
|
FILE *old = fopen(OLD_NNID_FILE, "a");
|
||||||
|
bool ok = true;
|
||||||
|
for (u8 slot = 1; slot <= count; slot++) {
|
||||||
|
if (slot == current) continue;
|
||||||
|
AccountId id = { 0 };
|
||||||
|
u32 pid = 0;
|
||||||
|
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) || !id[0]) continue;
|
||||||
|
ACT_GetAccountInfo(&pid, sizeof(pid), slot, INFO_TYPE_PRINCIPAL_ID);
|
||||||
|
if (old) fprintf(old, "Konto-Platz %u ID: %s PID: %lu (anderer Platz, z. B. Pretendo)\n", slot, id, (unsigned long)pid);
|
||||||
|
Result rc = ACTA_UnbindServerAccount(slot, true);
|
||||||
|
if (log) fprintf(log, "Platz %u (%s) lösen %08lX\n", slot, id, (unsigned long)rc);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "ID \"%s\" (Platz %u) konnte nicht gelöst werden\n(Fehler %08lX).", id, slot, (unsigned long)rc);
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (old) fclose(old);
|
||||||
|
if (log) fclose(log);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nnid_unlink(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (!ready) {
|
||||||
|
snprintf(msg, msglen, "Kontodienst nicht verfügbar.\nIst Luma3DS aktuell?");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
u8 slot = current_slot();
|
||||||
|
AccountId id = { 0 };
|
||||||
|
u32 pid = 0;
|
||||||
|
ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID);
|
||||||
|
ACT_GetAccountInfo(&pid, sizeof(pid), slot, INFO_TYPE_PRINCIPAL_ID);
|
||||||
|
if (!id[0]) return true; // nichts verknüpft
|
||||||
|
|
||||||
|
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||||
|
|
||||||
|
// Alte ID merken, damit der Name nicht verloren geht
|
||||||
|
FILE *f = fopen(OLD_NNID_FILE, "a");
|
||||||
|
if (f) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
char date[32];
|
||||||
|
strftime(date, sizeof(date), "%Y-%m-%d %H:%M", localtime(&now));
|
||||||
|
fprintf(f, "%s Konto-Platz %u NNID: %s PID: %lu\n", date, slot, id, (unsigned long)pid);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *log = fopen(LOG_FILE, "w");
|
||||||
|
if (log) fprintf(log, "MBMNET: NNID lösen (App v%s)\nKonto-Platz %u, NNID %s\n\n", APP_VERSION, slot, id);
|
||||||
|
|
||||||
|
Result rc = ACTA_UnbindServerAccount(slot, true);
|
||||||
|
if (log) fprintf(log, "%-30s %08lX\n", "Verknüpfung lösen", (unsigned long)rc);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
// Manche Zustände erlauben das nur bei abgemeldetem Konto: abmelden und erneut versuchen
|
||||||
|
Result u = ACTA_UnloadConsoleAccount();
|
||||||
|
if (log) fprintf(log, "%-30s %08lX\n", "Konto abmelden", (unsigned long)u);
|
||||||
|
rc = ACTA_UnbindServerAccount(slot, true);
|
||||||
|
if (log) fprintf(log, "%-30s %08lX\n", "Verknüpfung lösen (2.)", (unsigned long)rc);
|
||||||
|
}
|
||||||
|
if (log) { fprintf(log, "\nErgebnis: %s\n", R_SUCCEEDED(rc) ? "OK" : "FEHLER"); fclose(log); }
|
||||||
|
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "NNID konnte nicht gelöst werden (Fehler %08lX).\nProtokoll: /3ds/mbmnet/konto.log", (unsigned long)rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für die Diagnose: alle Konto-Plätze mit ihren Daten
|
||||||
|
void nnid_dump(FILE *f)
|
||||||
|
{
|
||||||
|
if (!ready) { fprintf(f, " Kontodienst nicht verfügbar\n"); return; }
|
||||||
|
u8 count = 0, current = 0, def = 0;
|
||||||
|
ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS);
|
||||||
|
ACT_GetCommonInfo(¤t, sizeof(current), INFO_TYPE_COMMON_CURRENT_ACCOUNT_SLOT);
|
||||||
|
ACT_GetCommonInfo(&def, sizeof(def), INFO_TYPE_COMMON_DEFAULT_ACCOUNT_SLOT);
|
||||||
|
u8 friendAccount = 0;
|
||||||
|
FRD_GetMyLocalAccountId(&friendAccount);
|
||||||
|
fprintf(f, " %u Plätze, aktiv: %u, Standard: %u, geladenes Freundekonto: %u (1 = Nintendo, 3 = MBMNET)\n",
|
||||||
|
count, current, def, friendAccount);
|
||||||
|
for (u8 slot = 1; slot <= count; slot++) {
|
||||||
|
AccountId id = { 0 }, assigned = { 0 };
|
||||||
|
u32 pid = 0, assignedPid = 0;
|
||||||
|
u8 fp = 0;
|
||||||
|
bool committed = false;
|
||||||
|
Result r1 = ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID);
|
||||||
|
ACT_GetAccountInfo(assigned, sizeof(assigned), slot, INFO_TYPE_ASSIGNED_ACCOUNT_ID);
|
||||||
|
ACT_GetAccountInfo(&pid, sizeof(pid), slot, INFO_TYPE_PRINCIPAL_ID);
|
||||||
|
ACT_GetAccountInfo(&assignedPid, sizeof(assignedPid), slot, INFO_TYPE_ASSIGNED_PRINCIPAL_ID);
|
||||||
|
ACT_GetAccountInfo(&fp, sizeof(fp), slot, INFO_TYPE_FP_LOCAL_ACCOUNT_ID);
|
||||||
|
ACT_GetAccountInfo(&committed, sizeof(committed), slot, INFO_TYPE_IS_COMMITTED);
|
||||||
|
fprintf(f, " Platz %u: ID \"%s\" (Länge %u, Abfrage %08lX) PID %lu zugewiesen \"%s\"/%lu Freunde-Konto %u gespeichert %d\n",
|
||||||
|
slot, id, (unsigned)strlen(id), (unsigned long)r1, (unsigned long)pid, assigned,
|
||||||
|
(unsigned long)assignedPid, fp, committed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gibt es die ID "name" in einem Konto-Platz außer exceptSlot?
|
||||||
|
bool nnid_name_elsewhere(const char *name, u8 exceptSlot)
|
||||||
|
{
|
||||||
|
if (!ready || !name[0]) return false;
|
||||||
|
u8 count = 0;
|
||||||
|
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return false;
|
||||||
|
for (u8 slot = 1; slot <= count; slot++) {
|
||||||
|
if (slot == exceptSlot) continue;
|
||||||
|
AccountId id = { 0 };
|
||||||
|
if (R_SUCCEEDED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) && !strcasecmp(id, name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Löst die ID "name" in allen Konto-Plätzen außer exceptSlot (nur auf dem 3DS). false + msg bei Fehler.
|
||||||
|
bool nnid_unlink_named(const char *name, u8 exceptSlot, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (!ready) return false;
|
||||||
|
u8 count = 0;
|
||||||
|
if (R_FAILED(ACT_GetCommonInfo(&count, sizeof(count), INFO_TYPE_COMMON_NUM_ACCOUNTS))) return false;
|
||||||
|
FILE *log = fopen(LOG_FILE, "a");
|
||||||
|
bool ok = true;
|
||||||
|
for (u8 slot = 1; slot <= count; slot++) {
|
||||||
|
if (slot == exceptSlot) continue;
|
||||||
|
AccountId id = { 0 };
|
||||||
|
if (R_FAILED(ACT_GetAccountInfo(id, sizeof(id), slot, INFO_TYPE_ACCOUNT_ID)) || strcasecmp(id, name)) continue;
|
||||||
|
Result rc = ACTA_UnbindServerAccount(slot, true);
|
||||||
|
if (log) fprintf(log, "Platz %u (%s) lösen (gleicher Name wie MBMNET) %08lX\n", slot, id, (unsigned long)rc);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "\"%s\" (Platz %u) ließ sich nicht lösen (Fehler %08lX).", id, slot, (unsigned long)rc);
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (log) fclose(log);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
Result nnid_init(void);
|
||||||
|
void nnid_exit(void);
|
||||||
|
// true, wenn auf dem aktuellen Konto eine NNID verknüpft ist (Name in name)
|
||||||
|
bool nnid_linked(char *name, size_t len);
|
||||||
|
// PID der verknüpften NNID (0 = keine)
|
||||||
|
u32 nnid_pid(void);
|
||||||
|
// IDs, die in ANDEREN Konto-Plätzen verknüpft sind (z. B. Pretendo). Anzahl, Namen in names.
|
||||||
|
int nnid_others(char *names, size_t len);
|
||||||
|
// Löst diese IDs lokal (Namen werden in /3ds/mbmnet/alte-nnid.txt gesichert)
|
||||||
|
bool nnid_unlink_others(char *msg, size_t msglen);
|
||||||
|
// Löst die verknüpfte NNID lokal (Name/PID werden in /3ds/mbmnet/alte-nnid.txt gesichert)
|
||||||
|
bool nnid_unlink(char *msg, size_t msglen);
|
||||||
|
// Gibt es die ID "name" in einem Konto-Platz außer exceptSlot?
|
||||||
|
bool nnid_name_elsewhere(const char *name, u8 exceptSlot);
|
||||||
|
// Löst die ID "name" in allen Konto-Plätzen außer exceptSlot (nur auf dem 3DS)
|
||||||
|
bool nnid_unlink_named(const char *name, u8 exceptSlot, char *msg, size_t msglen);
|
||||||
|
// Schreibt alle Konto-Plätze in die Diagnose
|
||||||
|
void nnid_dump(FILE *f);
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Miiverse-Patch: leitet die Miiverse-App auf MBMNET um und lässt sie der MBMNET-Zertifizierungsstelle (CA)
|
||||||
|
// vertrauen, denn ihre Seiten lädt sie nur per HTTPS.
|
||||||
|
//
|
||||||
|
// Die Code-Änderungen stammen aus Pretendos Nimbus (patches/miiverse, GPLv3, nach Stary und Sono): mit armips
|
||||||
|
// übersetzt, statt Pretendos Discovery-Adresse und GlobalSign-Zertifikat kommen MBMNET-Adresse und -CA hinein.
|
||||||
|
// Sie gelten für die letzte Version der Miiverse-App (alle Regionen gleich). Vorher wird geprüft, dass die
|
||||||
|
// Nintendo-Discovery-Adresse genau an der erwarteten Stelle steht - sonst wird nichts gepatcht.
|
||||||
|
//
|
||||||
|
// 0x176F28 fügt der HTTP-Verbindung die CA hinzu (httpC:AddRootCA), Länge der CA an Byte 72
|
||||||
|
// 0x1D7FF8 / 0x1D80EC / 0x38DC30 SD-Karte zusätzlich einhängen und wieder aushängen
|
||||||
|
// 0x1E3224 / 0x1E3468 der Browser lädt seine CA aus sdmc:/3ds/mbmnet-ca.pem
|
||||||
|
// 0x38DCB0 die CA selbst (DER, höchstens 889 Bytes)
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "olvpatch.h"
|
||||||
|
|
||||||
|
#define CODE_BASE 0x100000
|
||||||
|
#define DISCOVERY_ADDR 0x15748C
|
||||||
|
#define DISCOVERY_URL "https://discovery.olv.nintendo.net/v1/endpoint"
|
||||||
|
#define CA_ADDR 0x38DCB0
|
||||||
|
#define CA_MAX 889
|
||||||
|
#define CA_PEM_FILE "sdmc:/3ds/mbmnet-ca.pem"
|
||||||
|
|
||||||
|
static const u8 olv_176F28[] = { 0x3C, 0x00, 0x9F, 0xE5, 0x70, 0x4F, 0x1D, 0xEE, 0x0C, 0x10, 0x95, 0xE5, 0x14, 0x80, 0x95, 0xE5, 0x80, 0x00, 0xA4, 0xE5, 0x04, 0x10, 0x84, 0xE5, 0x08, 0x00, 0xA0, 0xE1, 0x24, 0x80, 0x9F, 0xE5, 0x24, 0x10, 0x9F, 0xE5, 0x08, 0x80, 0x84, 0xE5, 0x10, 0x10, 0x84, 0xE5, 0x08, 0x82, 0xA0, 0xE1, 0x0A, 0x80, 0x88, 0xE3, 0x0C, 0x80, 0x84, 0xE5, 0x32, 0x00, 0x00, 0xEF, 0x00, 0x00, 0xA0, 0xE1, 0x08, 0x00, 0x00, 0xEA, 0x82, 0x00, 0x24, 0x00, 0x79, 0x03, 0x00, 0x00, 0xB0, 0xDC, 0x38, 0x00, 0x00, 0x00, 0xA0, 0xE1, 0x00, 0x00, 0xA0, 0xE1, 0x00, 0x00, 0xA0, 0xE1 };
|
||||||
|
static const u8 olv_1D7FF8[] = { 0x0C, 0xD7, 0x06, 0xEA };
|
||||||
|
static const u8 olv_1D80EC[] = { 0xD4, 0xD6, 0x06, 0xEA };
|
||||||
|
static const u8 olv_1E3224[] = { 0x01, 0x20, 0xA0, 0xE3 };
|
||||||
|
static const u8 olv_1E3468[] = "3ds/mbmnet-ca.pem"; // mit Nullbyte: 18 Bytes
|
||||||
|
static const u8 olv_38DC30[] = { 0x00, 0x40, 0x2D, 0xE9, 0x2E, 0x28, 0xF9, 0xEB, 0x18, 0x00, 0x9F, 0xE5, 0x00, 0x40, 0xBD, 0xE8, 0xB6, 0x93, 0xF8, 0xEA, 0x00, 0x40, 0x2D, 0xE9, 0x02, 0x29, 0xF9, 0xEB, 0x04, 0x00, 0x9F, 0xE5, 0x00, 0x40, 0xBD, 0xE8, 0xBB, 0x92, 0xFA, 0xEA, 0x9C, 0x81, 0x1D, 0x00 };
|
||||||
|
#define CA_LEN_BYTE 72 // in olv_176F28: Länge der CA (Literal-Pool)
|
||||||
|
|
||||||
|
// Lädt eine Datei vom MBMNET-Server. Gibt die Länge zurück, 0 bei Fehler.
|
||||||
|
static u32 fetch(const char *server, const char *path, u8 *buf, u32 cap)
|
||||||
|
{
|
||||||
|
char url[160];
|
||||||
|
snprintf(url, sizeof(url), "http://%s%s", server, path);
|
||||||
|
httpcContext ctx;
|
||||||
|
if (R_FAILED(httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1))) return 0;
|
||||||
|
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
|
||||||
|
u32 status = 0, got = 0;
|
||||||
|
Result rc = httpcBeginRequest(&ctx);
|
||||||
|
if (R_SUCCEEDED(rc)) rc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 8ULL * 1000 * 1000 * 1000);
|
||||||
|
if (R_SUCCEEDED(rc) && status == 200) rc = httpcDownloadData(&ctx, buf, cap, &got);
|
||||||
|
httpcCloseContext(&ctx);
|
||||||
|
return R_SUCCEEDED(rc) && status == 200 && got < cap ? got : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen)
|
||||||
|
{
|
||||||
|
static u8 pem[2048];
|
||||||
|
u32 n = fetch(server, "/olv/ca.der", der, CA_MAX + 1);
|
||||||
|
if (n == 0 || n > CA_MAX) return false;
|
||||||
|
u32 p = fetch(server, "/olv/ca.pem", pem, sizeof(pem));
|
||||||
|
if (p == 0) return false;
|
||||||
|
// Der Browser der Miiverse-App liest die CA von der SD-Karte
|
||||||
|
FILE *f = fopen(CA_PEM_FILE, "wb");
|
||||||
|
bool ok = f && fwrite(pem, 1, p, f) == p;
|
||||||
|
if (f) fclose(f);
|
||||||
|
*derLen = n;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool olvpatch_supported(const u8 *code, u32 size)
|
||||||
|
{
|
||||||
|
u32 off = DISCOVERY_ADDR - CODE_BASE;
|
||||||
|
return off + sizeof(DISCOVERY_URL) <= size && memcmp(code + off, DISCOVERY_URL, sizeof(DISCOVERY_URL)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static u32 ips_record(u8 *out, u32 n, u32 cap, u32 addr, const u8 *data, u32 len)
|
||||||
|
{
|
||||||
|
if (n + 5 + len + 3 > cap) return 0;
|
||||||
|
u32 off = addr - CODE_BASE;
|
||||||
|
u8 head[5] = { off >> 16, off >> 8, off, len >> 8, len };
|
||||||
|
memcpy(out + n, head, 5);
|
||||||
|
memcpy(out + n + 5, data, len);
|
||||||
|
return n + 5 + len;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 olvpatch_build(const char *server, const u8 *der, u32 derLen, u8 *out, u32 cap)
|
||||||
|
{
|
||||||
|
// Discovery: "https://discovery.olv.nintendo.net/v1/endpoint" -> "http://<server>/v1/endpoint", Rest Nullbytes
|
||||||
|
u8 discovery[sizeof(DISCOVERY_URL)] = { 0 };
|
||||||
|
int len = snprintf((char *)discovery, sizeof(discovery), "http://%s/v1/endpoint", server);
|
||||||
|
if (len <= 0 || len >= (int)sizeof(discovery) || derLen == 0 || derLen > CA_MAX) return 0;
|
||||||
|
|
||||||
|
u8 caHook[sizeof(olv_176F28)];
|
||||||
|
memcpy(caHook, olv_176F28, sizeof(caHook));
|
||||||
|
caHook[CA_LEN_BYTE] = derLen;
|
||||||
|
caHook[CA_LEN_BYTE + 1] = derLen >> 8;
|
||||||
|
|
||||||
|
u32 n = 5;
|
||||||
|
memcpy(out, "PATCH", 5);
|
||||||
|
struct { u32 addr; const u8 *data; u32 len; } recs[] = {
|
||||||
|
{ DISCOVERY_ADDR, discovery, sizeof(discovery) },
|
||||||
|
{ 0x176F28, caHook, sizeof(caHook) },
|
||||||
|
{ 0x1D7FF8, olv_1D7FF8, sizeof(olv_1D7FF8) },
|
||||||
|
{ 0x1D80EC, olv_1D80EC, sizeof(olv_1D80EC) },
|
||||||
|
{ 0x1E3224, olv_1E3224, sizeof(olv_1E3224) },
|
||||||
|
{ 0x1E3468, olv_1E3468, sizeof(olv_1E3468) },
|
||||||
|
{ 0x38DC30, olv_38DC30, sizeof(olv_38DC30) },
|
||||||
|
{ CA_ADDR, der, derLen },
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(recs) / sizeof(recs[0]); i++)
|
||||||
|
if (!(n = ips_record(out, n, cap, recs[i].addr, recs[i].data, recs[i].len))) return 0;
|
||||||
|
memcpy(out + n, "EOF", 3);
|
||||||
|
return n + 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
// Miiverse-Patch (siehe olvpatch.c). Die CA kommt vom MBMNET-Server (/olv/ca.der, /olv/ca.pem).
|
||||||
|
|
||||||
|
// Holt die MBMNET-CA (DER, höchstens 889 Bytes) und legt die PEM-Fassung für den Browser auf die SD.
|
||||||
|
bool olvpatch_fetch_ca(const char *server, u8 *der, u32 *derLen);
|
||||||
|
// Ist das genau die Miiverse-Version, für die der Patch gebaut ist? (entpackter .code)
|
||||||
|
bool olvpatch_supported(const u8 *code, u32 size);
|
||||||
|
// Baut den IPS-Patch. Gibt die Länge zurück, 0 bei Fehler.
|
||||||
|
u32 olvpatch_build(const char *server, const u8 *der, u32 derLen, u8 *out, u32 cap);
|
||||||
@@ -0,0 +1,885 @@
|
|||||||
|
// Erzeugt und verwaltet die MBMNET-Patches.
|
||||||
|
//
|
||||||
|
// Gepatcht werden zwei Systemmodule:
|
||||||
|
// Freunde (0004013000003202): https://nasc.nintendowifi.net -> http://<server>
|
||||||
|
// Konto (0004013000003802): https://account.nintendo.net -> http://<server>
|
||||||
|
//
|
||||||
|
// Ablauf je Modul:
|
||||||
|
// 1. .code lesen (direkt aus dem NAND, sonst aus einem Dump auf der SD)
|
||||||
|
// 2. falls komprimiert: entpacken (Luma patcht die entpackte Version)
|
||||||
|
// 3. alle Nintendo-Adressen suchen und durch die MBMNET-Adresse ersetzen
|
||||||
|
// 4. als IPS-Patch nach /luma/titles/<TID>/code.ips schreiben
|
||||||
|
//
|
||||||
|
// Am NAND wird nie etwas verändert. Luma3DS wendet die Patches nur beim Laden an.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
|
||||||
|
#include "blz.h"
|
||||||
|
#include "patcher.h"
|
||||||
|
#include "lumaconfig.h"
|
||||||
|
#include "olvpatch.h"
|
||||||
|
#include "nnid.h"
|
||||||
|
|
||||||
|
#define SYSMODULE_DIR "sdmc:/luma/sysmodules"
|
||||||
|
|
||||||
|
#define MAX_HITS 8
|
||||||
|
|
||||||
|
// Boot-Splash: Luma zeigt beim Einschalten "MBMNET aktiv" (Bild in splash_data.c)
|
||||||
|
#define SPLASH_FILE "sdmc:/luma/splash.bin"
|
||||||
|
#define SPLASH_BACKUP "sdmc:/luma/splash.bin.vor-mbmnet"
|
||||||
|
#define SPLASH_SIZE (400 * 240 * 3)
|
||||||
|
|
||||||
|
// Zwischenspeicher: fertige Patches und der Splash. Damit ist Umschalten nur noch Umbenennen und
|
||||||
|
// zwei kleine Dateien schreiben, statt die Module aus dem NAND zu lesen, zu entpacken und zu durchsuchen.
|
||||||
|
// Ein Patch gilt nur, solange Modul (Größe + Ende der .code), Server und Regeln gleich sind.
|
||||||
|
#define CACHE_DIR APP_DIR "/cache"
|
||||||
|
#define SPLASH_CACHE CACHE_DIR "/splash-" APP_VERSION ".bin"
|
||||||
|
#define RULES_VERSION "1"
|
||||||
|
extern const u8 splash_rle[];
|
||||||
|
extern const u32 splash_rle_size;
|
||||||
|
|
||||||
|
// Eine Ersetzungsregel. Es gibt zwei Arten:
|
||||||
|
// PREFIX: Text beginnt mit needle -> needle wird zu "http://<server>", der Rest bleibt
|
||||||
|
// ("https://nasc.nintendowifi.net/ac" -> "http://<server>/ac")
|
||||||
|
// EXACT: Text ist genau needle -> wird zu "http://<server>" + suffix
|
||||||
|
// (für Vorlagen mit Platzhaltern, deren Werte der 3DS dann ins Leere schreibt)
|
||||||
|
typedef enum { RULE_PREFIX, RULE_EXACT } RuleType;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
RuleType type;
|
||||||
|
const char *needle;
|
||||||
|
const char *suffix; // nur EXACT
|
||||||
|
} Rule;
|
||||||
|
|
||||||
|
#define MAX_RULES 2
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
u64 tid;
|
||||||
|
const char *dir; // Luma-Ordner für dieses Modul
|
||||||
|
const char *label; // Name für Meldungen
|
||||||
|
Rule rules[MAX_RULES];
|
||||||
|
bool optional; // Anwendung statt Systemmodul: nur patchen, wenn installiert und Adresse gefunden
|
||||||
|
} Target;
|
||||||
|
|
||||||
|
#define TITLES_DIR "sdmc:/luma/titles"
|
||||||
|
|
||||||
|
// Umgelenkt wird die normale Nintendo-Umgebung: Alles, was der 3DS sonst zu
|
||||||
|
// nasc.nintendowifi.net und account.nintendo.net schickt, geht an MBMNET.
|
||||||
|
static const Target targets[] = {
|
||||||
|
{ 0x0004013000003202ULL, SYSMODULE_DIR, "Freunde", {
|
||||||
|
{ RULE_PREFIX, "https://nasc.nintendowifi.net" },
|
||||||
|
} },
|
||||||
|
{ 0x0004013000003802ULL, SYSMODULE_DIR, "Konto", {
|
||||||
|
// Aktuelle Firmware: die beiden %s sind Zusätze für Nintendos Test-Server (sonst leer),
|
||||||
|
// %u ist die API-Version (immer 1).
|
||||||
|
{ RULE_EXACT, "https://%s%saccount.nintendo.net/v%u/api/", "/v1/api/" },
|
||||||
|
{ RULE_PREFIX, "https://account.nintendo.net" },
|
||||||
|
} },
|
||||||
|
// Miiverse (JPN, USA, EUR): fragt zuerst den Discovery-Server, wohin es geht, und lädt dann seine Seiten
|
||||||
|
// per HTTPS. Gebaut wird der Patch in olvpatch.c (die Regel hier dient nur der Diagnose). Nur die
|
||||||
|
// installierte Region wird gepatcht. Luma lädt Patches für Anwendungen aus /luma/titles/<ID>/code.ips (wie Nimbus).
|
||||||
|
{ 0x000400300000BC02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true },
|
||||||
|
{ 0x000400300000BD02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true },
|
||||||
|
{ 0x000400300000BE02ULL, TITLES_DIR, "Miiverse", { { RULE_PREFIX, "https://discovery.olv.nintendo.net" } }, true },
|
||||||
|
};
|
||||||
|
#define TARGET_COUNT (sizeof(targets) / sizeof(targets[0]))
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
u32 offset;
|
||||||
|
u32 size; // Anzahl Bytes, die geschrieben werden
|
||||||
|
u8 data[160];
|
||||||
|
} Hit;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Hit hits[MAX_HITS];
|
||||||
|
int count;
|
||||||
|
} Patch;
|
||||||
|
|
||||||
|
// --- Dateien ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
static bool file_exists(const char *path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
return stat(path, &st) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luma3DS lädt IPS-Patches für Systemmodule aus /luma/sysmodules/<Titel-ID>.ips
|
||||||
|
// (nur wenn "Enable loading external FIRMs and system modules" an ist).
|
||||||
|
// Daneben legen wir einen Merker <ID>.mbmnet und ggf. die Sicherung eines fremden Patches ab.
|
||||||
|
static void path_in(char *out, size_t len, const Target *t, const char *file)
|
||||||
|
{
|
||||||
|
// Anwendungen: /luma/titles/<ID>/code.ips (+ mbmnet.txt, code.ips.vor-mbmnet)
|
||||||
|
if (strcmp(t->dir, TITLES_DIR) == 0) {
|
||||||
|
snprintf(out, len, "%s/%016llX/%s", t->dir, t->tid, file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *ext = strcmp(file, "code.ips") == 0 ? ".ips"
|
||||||
|
: strcmp(file, "mbmnet.txt") == 0 ? ".mbmnet"
|
||||||
|
: ".ips.vor-mbmnet";
|
||||||
|
snprintf(out, len, "%s/%016llX%s", t->dir, t->tid, ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bis v2.2.0 lagen die Patches unter /luma/titles/<ID>/code.ips - dort wendet Luma sie für
|
||||||
|
// Systemmodule aber nicht an. Alte Dateien entfernen und gesicherte fremde Patches zurückholen.
|
||||||
|
bool patch_migrate(void)
|
||||||
|
{
|
||||||
|
bool migrated = false;
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
if (targets[i].optional) continue; // Anwendungen gehören nach /luma/titles
|
||||||
|
char dir[64], ips[96], marker[96], backup[96];
|
||||||
|
snprintf(dir, sizeof(dir), "sdmc:/luma/titles/%016llX", targets[i].tid);
|
||||||
|
snprintf(ips, sizeof(ips), "%s/code.ips", dir);
|
||||||
|
snprintf(marker, sizeof(marker), "%s/mbmnet.txt", dir);
|
||||||
|
snprintf(backup, sizeof(backup), "%s/code.ips.vor-mbmnet", dir);
|
||||||
|
if (!file_exists(marker)) continue;
|
||||||
|
remove(ips);
|
||||||
|
remove(marker);
|
||||||
|
if (file_exists(backup)) rename(backup, ips);
|
||||||
|
migrated = true;
|
||||||
|
}
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mkdirs(const char *path)
|
||||||
|
{
|
||||||
|
char tmp[256];
|
||||||
|
snprintf(tmp, sizeof(tmp), "%s", path);
|
||||||
|
for (char *p = tmp + strlen("sdmc:/"); *p; p++) {
|
||||||
|
if (*p == '/') {
|
||||||
|
*p = 0;
|
||||||
|
mkdir(tmp, 0777);
|
||||||
|
*p = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mkdir(tmp, 0777);
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchState patch_state(void)
|
||||||
|
{
|
||||||
|
char path[128];
|
||||||
|
bool foreign = false;
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
path_in(path, sizeof(path), &targets[i], "mbmnet.txt");
|
||||||
|
if (file_exists(path)) return STATE_MBMNET;
|
||||||
|
path_in(path, sizeof(path), &targets[i], "code.ips");
|
||||||
|
if (!targets[i].optional && file_exists(path)) foreign = true;
|
||||||
|
}
|
||||||
|
return foreign ? STATE_FOREIGN : STATE_ORIGINAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- .code lesen ------------------------------------------------------------
|
||||||
|
|
||||||
|
static Result read_from_nand(u64 tid, u8 **out, u32 *outSize)
|
||||||
|
{
|
||||||
|
u32 archPath[4] = { (u32)tid, (u32)(tid >> 32), MEDIATYPE_NAND, 0 };
|
||||||
|
// ExeFS-Datei ".code"
|
||||||
|
static const u32 filePath[5] = { 0, 0, 2, 0x646F632E, 0x00000065 };
|
||||||
|
FS_Path arch = { PATH_BINARY, sizeof(archPath), archPath };
|
||||||
|
FS_Path file = { PATH_BINARY, sizeof(filePath), filePath };
|
||||||
|
|
||||||
|
Handle h;
|
||||||
|
Result rc = FSUSER_OpenFileDirectly(&h, ARCHIVE_SAVEDATA_AND_CONTENT, arch, file, FS_OPEN_READ, 0);
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
|
||||||
|
u64 size = 0;
|
||||||
|
rc = FSFILE_GetSize(h, &size);
|
||||||
|
if (R_SUCCEEDED(rc)) {
|
||||||
|
u8 *buf = malloc(size);
|
||||||
|
u32 read = 0;
|
||||||
|
if (!buf) rc = -1;
|
||||||
|
else rc = FSFILE_Read(h, &read, 0, buf, size);
|
||||||
|
if (R_SUCCEEDED(rc)) { *out = buf; *outSize = read; }
|
||||||
|
else free(buf);
|
||||||
|
}
|
||||||
|
FSFILE_Close(h);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ersatzweise ein Dump von der SD: /3ds/mbmnet/<TID>.bin
|
||||||
|
static bool read_from_sd(u64 tid, u8 **out, u32 *outSize)
|
||||||
|
{
|
||||||
|
char path[64];
|
||||||
|
snprintf(path, sizeof(path), APP_DIR "/%016llX.bin", tid);
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return false;
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long size = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
u8 *buf = size > 0 ? malloc(size) : NULL;
|
||||||
|
bool ok = buf && fread(buf, 1, size, f) == (size_t)size;
|
||||||
|
fclose(f);
|
||||||
|
if (!ok) { free(buf); return false; }
|
||||||
|
*out = buf;
|
||||||
|
*outSize = size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool load_code(const Target *t, u8 **out, u32 *outSize, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
u8 *buf = NULL;
|
||||||
|
u32 size = 0;
|
||||||
|
Result rc = read_from_nand(t->tid, &buf, &size);
|
||||||
|
if (R_FAILED(rc) && t->optional) return false; // nicht installiert (andere Region)
|
||||||
|
if (R_FAILED(rc) && !read_from_sd(t->tid, &buf, &size)) {
|
||||||
|
snprintf(msg, msglen,
|
||||||
|
"%s-Modul nicht lesbar (Fehler %08lX).\n"
|
||||||
|
"Lege einen Dump als /3ds/mbmnet/%016llX.bin\nab (siehe Anleitung).",
|
||||||
|
t->label, (unsigned long)rc, t->tid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blz_looks_compressed(buf, size)) {
|
||||||
|
u32 full = blz_decompressed_size(buf, size);
|
||||||
|
u8 *big = realloc(buf, full);
|
||||||
|
if (!big) { free(buf); snprintf(msg, msglen, "Zu wenig Speicher."); return false; }
|
||||||
|
buf = big;
|
||||||
|
if (!blz_decompress(buf, size)) {
|
||||||
|
free(buf);
|
||||||
|
snprintf(msg, msglen, "%s-Modul konnte nicht entpackt werden.", t->label);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size = full;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = buf;
|
||||||
|
*outSize = size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Zwischenspeicher ------------------------------------------------------------
|
||||||
|
|
||||||
|
// Fingerabdruck des Moduls ohne es ganz zu lesen: Größe und die letzten 16 Bytes der .code
|
||||||
|
// (bei komprimierten Modulen stehen dort die Entpack-Angaben). Leer, wenn nicht lesbar.
|
||||||
|
static void fingerprint(u64 tid, char *out, size_t len)
|
||||||
|
{
|
||||||
|
u32 archPath[4] = { (u32)tid, (u32)(tid >> 32), MEDIATYPE_NAND, 0 };
|
||||||
|
static const u32 filePath[5] = { 0, 0, 2, 0x646F632E, 0x00000065 };
|
||||||
|
out[0] = 0;
|
||||||
|
Handle h;
|
||||||
|
if (R_FAILED(FSUSER_OpenFileDirectly(&h, ARCHIVE_SAVEDATA_AND_CONTENT, (FS_Path){ PATH_BINARY, sizeof(archPath), archPath },
|
||||||
|
(FS_Path){ PATH_BINARY, sizeof(filePath), filePath }, FS_OPEN_READ, 0))) return;
|
||||||
|
u64 size = 0;
|
||||||
|
static u8 block[0x400];
|
||||||
|
u32 got = 0;
|
||||||
|
// Nur ab einer 0x200-Grenze lesen: Ein Lesen mitten im Block (Ende - 16) scheitert bei diesen Dateien
|
||||||
|
u64 start = size;
|
||||||
|
if (R_SUCCEEDED(FSFILE_GetSize(h, &size)) && size >= 16) {
|
||||||
|
start = (size - 16) & ~0x1FFULL;
|
||||||
|
if (R_SUCCEEDED(FSFILE_Read(h, &got, start, block, (u32)(size - start))) && got == size - start) {
|
||||||
|
const u8 *tail = block + got - 16;
|
||||||
|
int n = snprintf(out, len, "%llX-", size);
|
||||||
|
for (int i = 0; i < 16 && n + 2 < (int)len; i++) n += snprintf(out + n, len - n, "%02X", tail[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FSFILE_Close(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cache_key(const Target *t, const char *server, char *out, size_t len)
|
||||||
|
{
|
||||||
|
char fp[64];
|
||||||
|
fingerprint(t->tid, fp, sizeof(fp));
|
||||||
|
if (fp[0]) snprintf(out, len, "%s|%s|" RULES_VERSION, fp, server);
|
||||||
|
else out[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static u32 cache_load(const Target *t, const char *key, u8 *data, u32 cap)
|
||||||
|
{
|
||||||
|
char path[96], stored[160] = "";
|
||||||
|
if (!key[0]) return 0;
|
||||||
|
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.key", t->tid);
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (!f) return 0;
|
||||||
|
if (!fgets(stored, sizeof(stored), f)) stored[0] = 0;
|
||||||
|
fclose(f);
|
||||||
|
stored[strcspn(stored, "\r\n")] = 0;
|
||||||
|
if (strcmp(stored, key)) return 0;
|
||||||
|
|
||||||
|
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.ips", t->tid);
|
||||||
|
f = fopen(path, "rb");
|
||||||
|
u32 n = f ? fread(data, 1, cap, f) : 0;
|
||||||
|
if (f) fclose(f);
|
||||||
|
return n > 8 && !memcmp(data, "PATCH", 5) && !memcmp(data + n - 3, "EOF", 3) ? n : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cache_store(const Target *t, const char *key, const u8 *data, u32 n)
|
||||||
|
{
|
||||||
|
char path[96];
|
||||||
|
if (!key[0]) return;
|
||||||
|
mkdir(APP_DIR, 0777);
|
||||||
|
mkdir(CACHE_DIR, 0777);
|
||||||
|
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.ips", t->tid);
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
bool ok = f && fwrite(data, 1, n, f) == n;
|
||||||
|
if (f) fclose(f);
|
||||||
|
snprintf(path, sizeof(path), CACHE_DIR "/%016llX.key", t->tid);
|
||||||
|
f = ok ? fopen(path, "w") : NULL;
|
||||||
|
if (f) { fprintf(f, "%s\n", key); fclose(f); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Patch berechnen und schreiben ----------------------------------------------
|
||||||
|
|
||||||
|
static bool build_patch(const Target *t, const char *server, Patch *p, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
u8 *code;
|
||||||
|
u32 size;
|
||||||
|
if (!load_code(t, &code, &size, msg, msglen)) return false;
|
||||||
|
|
||||||
|
p->count = 0;
|
||||||
|
|
||||||
|
for (u32 i = 0; i < size && p->count < MAX_HITS; i++) {
|
||||||
|
for (int r = 0; r < MAX_RULES && t->rules[r].needle; r++) {
|
||||||
|
const Rule *rule = &t->rules[r];
|
||||||
|
size_t needleLen = strlen(rule->needle);
|
||||||
|
if (i + needleLen >= size || memcmp(code + i, rule->needle, needleLen) != 0) continue;
|
||||||
|
u32 len = strnlen((const char *)code + i, size - i);
|
||||||
|
|
||||||
|
Hit *h = &p->hits[p->count];
|
||||||
|
memset(h, 0, sizeof(*h));
|
||||||
|
|
||||||
|
if (rule->type == RULE_EXACT && len != needleLen) continue; // Vorlage muss genau passen
|
||||||
|
{
|
||||||
|
char repl[128];
|
||||||
|
if (rule->type == RULE_EXACT)
|
||||||
|
snprintf(repl, sizeof(repl), "http://%s%s", server, rule->suffix);
|
||||||
|
else
|
||||||
|
snprintf(repl, sizeof(repl), "http://%s%.*s",
|
||||||
|
server, (int)(len - needleLen), (const char *)code + i + needleLen);
|
||||||
|
if (len + 1 > sizeof(h->data) || strlen(repl) > len) {
|
||||||
|
free(code);
|
||||||
|
snprintf(msg, msglen, "Server-Adresse passt nicht in das %s-Modul\n(zu lang).", t->label);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Ersatz + Nullbytes bis zur Originallänge (inkl. Terminator)
|
||||||
|
h->offset = i;
|
||||||
|
h->size = len + 1;
|
||||||
|
memcpy(h->data, repl, strlen(repl));
|
||||||
|
}
|
||||||
|
p->count++;
|
||||||
|
i += len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(code);
|
||||||
|
|
||||||
|
if (p->count == 0 && t->optional) return false;
|
||||||
|
if (p->count == 0) {
|
||||||
|
char diag[64];
|
||||||
|
patch_diagnose(diag, sizeof(diag));
|
||||||
|
snprintf(msg, msglen, "%s-Modul: Nintendo-Adresse nicht gefunden.\n"
|
||||||
|
"Diagnose gespeichert: /3ds/mbmnet/diagnose.txt\nBitte an den Entwickler schicken.", t->label);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baut den IPS-Patch im Speicher. Gibt die Länge zurück.
|
||||||
|
static u32 build_ips(const Patch *p, u8 *out, u32 cap)
|
||||||
|
{
|
||||||
|
u32 n = 0;
|
||||||
|
memcpy(out, "PATCH", 5);
|
||||||
|
n = 5;
|
||||||
|
for (int i = 0; i < p->count && n + 5 + p->hits[i].size + 3 <= cap; i++) {
|
||||||
|
const Hit *h = &p->hits[i];
|
||||||
|
u8 rec[5] = { h->offset >> 16, h->offset >> 8, h->offset, h->size >> 8, h->size };
|
||||||
|
memcpy(out + n, rec, 5);
|
||||||
|
memcpy(out + n + 5, h->data, h->size);
|
||||||
|
n += 5 + h->size;
|
||||||
|
}
|
||||||
|
memcpy(out + n, "EOF", 3);
|
||||||
|
return n + 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schreibt den Patch. *changed wird true, wenn sich die Datei dadurch geändert hat
|
||||||
|
// (nur dann muss der 3DS neu starten, denn Luma patcht beim Laden der Module).
|
||||||
|
#define IPS_CAP 2048 // Miiverse-Patch mit CA: ca. 1100 Bytes
|
||||||
|
|
||||||
|
static bool write_ips(const char *path, const u8 *data, u32 n, bool *changed)
|
||||||
|
{
|
||||||
|
static u8 old[IPS_CAP];
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
size_t oldLen = f ? fread(old, 1, sizeof(old), f) : 0;
|
||||||
|
if (f) fclose(f);
|
||||||
|
if (oldLen == n && memcmp(old, data, n) == 0) return true; // schon genau so vorhanden
|
||||||
|
|
||||||
|
f = fopen(path, "wb");
|
||||||
|
bool ok = f && fwrite(data, 1, n, f) == n;
|
||||||
|
if (f) fclose(f);
|
||||||
|
if (ok) *changed = true;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miiverse-Patch (olvpatch.c): eigene Code-Änderungen + MBMNET-CA statt einfacher Text-Regeln.
|
||||||
|
// 0 = nicht installiert, andere Version oder CA nicht vom Server zu holen (dann bleibt ein alter Patch stehen).
|
||||||
|
static u32 build_miiverse(const Target *t, const char *server, u8 *out, u32 cap)
|
||||||
|
{
|
||||||
|
static u8 der[1024];
|
||||||
|
static u32 derLen;
|
||||||
|
static int caState; // 0 = noch nicht geholt, 1 = da, -1 = nicht zu holen
|
||||||
|
char fp[64];
|
||||||
|
fingerprint(t->tid, fp, sizeof(fp)); // leer = kein Zwischenspeicher, gebaut wird trotzdem
|
||||||
|
if (caState == 0) caState = olvpatch_fetch_ca(server, der, &derLen) ? 1 : -1;
|
||||||
|
if (caState < 0) return 0;
|
||||||
|
|
||||||
|
u32 sum = 2166136261u; // FNV-1a über die CA: neue CA -> neuer Patch
|
||||||
|
for (u32 i = 0; i < derLen; i++) sum = (sum ^ der[i]) * 16777619u;
|
||||||
|
char key[160];
|
||||||
|
if (fp[0]) snprintf(key, sizeof(key), "%s|%s|olv1|%08lX", fp, server, (unsigned long)sum);
|
||||||
|
else key[0] = 0;
|
||||||
|
u32 n = cache_load(t, key, out, cap);
|
||||||
|
if (n) return n;
|
||||||
|
|
||||||
|
u8 *code;
|
||||||
|
u32 size;
|
||||||
|
char ignore[8];
|
||||||
|
if (!load_code(t, &code, &size, ignore, sizeof(ignore))) return 0;
|
||||||
|
bool supported = olvpatch_supported(code, size);
|
||||||
|
free(code);
|
||||||
|
if (!supported) return 0;
|
||||||
|
n = olvpatch_build(server, der, derLen, out, cap);
|
||||||
|
if (n) cache_store(t, key, out, n);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fremde Patches, die MBMNET stören ---------------------------------------------
|
||||||
|
// Pretendos Patch für das http-Modul fragt bei jeder Verbindung den Freunde-Dienst nach dem
|
||||||
|
// aktiven Konto. Stellt der Freunde-Dienst selbst die Anfrage, warten beide aufeinander und
|
||||||
|
// keine Anfrage kommt bei MBMNET an. Er wird beim Aktivieren beiseitegelegt und beim
|
||||||
|
// Zurücksetzen zurückgeholt.
|
||||||
|
static const u64 conflicting[] = {
|
||||||
|
0x0004013000002902ULL, // http
|
||||||
|
};
|
||||||
|
|
||||||
|
static int park_conflicting(void)
|
||||||
|
{
|
||||||
|
int parked = 0;
|
||||||
|
for (size_t i = 0; i < sizeof(conflicting) / sizeof(conflicting[0]); i++) {
|
||||||
|
char ips[96], backup[112];
|
||||||
|
snprintf(ips, sizeof(ips), SYSMODULE_DIR "/%016llX.ips", conflicting[i]);
|
||||||
|
snprintf(backup, sizeof(backup), "%s.vor-mbmnet", ips);
|
||||||
|
// Liegt hier ein Patch, ist er fremd (MBMNET legt hier keinen an): immer den aktuellen sichern
|
||||||
|
if (file_exists(ips)) {
|
||||||
|
remove(backup);
|
||||||
|
if (rename(ips, backup) == 0) parked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parked;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void restore_conflicting(void)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < sizeof(conflicting) / sizeof(conflicting[0]); i++) {
|
||||||
|
char ips[96], backup[112];
|
||||||
|
snprintf(ips, sizeof(ips), SYSMODULE_DIR "/%016llX.ips", conflicting[i]);
|
||||||
|
snprintf(backup, sizeof(backup), "%s.vor-mbmnet", ips);
|
||||||
|
if (file_exists(backup) && !file_exists(ips)) rename(backup, ips);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Boot-Splash --------------------------------------------------------------------
|
||||||
|
|
||||||
|
static bool install_splash(bool alreadyOurs)
|
||||||
|
{
|
||||||
|
// Eigenen Splash des Nutzers sichern (nur beim ersten Aktivieren)
|
||||||
|
if (!alreadyOurs && file_exists(SPLASH_FILE) && !file_exists(SPLASH_BACKUP)) rename(SPLASH_FILE, SPLASH_BACKUP);
|
||||||
|
if (alreadyOurs && file_exists(SPLASH_FILE)) return true;
|
||||||
|
// Beim Deaktivieren geparkt: nur zurückschieben
|
||||||
|
if (!file_exists(SPLASH_FILE) && file_exists(SPLASH_CACHE) && rename(SPLASH_CACHE, SPLASH_FILE) == 0) return true;
|
||||||
|
|
||||||
|
u8 *buf = malloc(SPLASH_SIZE);
|
||||||
|
if (!buf) return false;
|
||||||
|
u32 out = 0;
|
||||||
|
for (u32 i = 0; i + 3 < splash_rle_size && out < SPLASH_SIZE; i += 4) {
|
||||||
|
for (u8 n = splash_rle[i]; n > 0 && out + 3 <= SPLASH_SIZE; n--) {
|
||||||
|
memcpy(buf + out, &splash_rle[i + 1], 3);
|
||||||
|
out += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FILE *f = out == SPLASH_SIZE ? fopen(SPLASH_FILE, "wb") : NULL;
|
||||||
|
bool ok = f && fwrite(buf, 1, SPLASH_SIZE, f) == SPLASH_SIZE;
|
||||||
|
if (f) fclose(f);
|
||||||
|
free(buf);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_splash(void)
|
||||||
|
{
|
||||||
|
// Parken statt löschen, damit das nächste Aktivieren ihn nicht neu schreiben muss
|
||||||
|
mkdir(APP_DIR, 0777);
|
||||||
|
mkdir(CACHE_DIR, 0777);
|
||||||
|
remove(SPLASH_CACHE);
|
||||||
|
if (rename(SPLASH_FILE, SPLASH_CACHE) != 0) remove(SPLASH_FILE);
|
||||||
|
if (file_exists(SPLASH_BACKUP)) rename(SPLASH_BACKUP, SPLASH_FILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Aktivieren / Deaktivieren -----------------------------------------------
|
||||||
|
|
||||||
|
bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
*changed = false;
|
||||||
|
if (!server[0]) {
|
||||||
|
snprintf(msg, msglen, "Bitte zuerst die Server-Adresse eintragen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool alreadyOurs = patch_state() == STATE_MBMNET;
|
||||||
|
|
||||||
|
// Erst alle Patches bereitlegen (aus dem Zwischenspeicher oder neu berechnet):
|
||||||
|
// Klappt eines nicht, wird gar nichts geschrieben.
|
||||||
|
static u8 ips[TARGET_COUNT][IPS_CAP];
|
||||||
|
u32 ipsLen[TARGET_COUNT];
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
if (targets[i].optional) { // Miiverse
|
||||||
|
ipsLen[i] = build_miiverse(&targets[i], server, ips[i], IPS_CAP);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char key[160];
|
||||||
|
cache_key(&targets[i], server, key, sizeof(key));
|
||||||
|
ipsLen[i] = cache_load(&targets[i], key, ips[i], IPS_CAP);
|
||||||
|
if (ipsLen[i]) continue;
|
||||||
|
static Patch patch;
|
||||||
|
if (!build_patch(&targets[i], server, &patch, msg, msglen)) {
|
||||||
|
if (targets[i].optional) { ipsLen[i] = 0; continue; } // nicht installiert: auslassen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ipsLen[i] = build_ips(&patch, ips[i], IPS_CAP);
|
||||||
|
cache_store(&targets[i], key, ips[i], ipsLen[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool backedUp = false, appPatched = false;
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
const Target *t = &targets[i];
|
||||||
|
if (!ipsLen[i]) continue;
|
||||||
|
char path[128], marker[128], backup[128], dir[96];
|
||||||
|
if (t->optional) {
|
||||||
|
snprintf(dir, sizeof(dir), "%s/%016llX", t->dir, t->tid);
|
||||||
|
mkdirs(dir);
|
||||||
|
appPatched = true;
|
||||||
|
}
|
||||||
|
path_in(path, sizeof(path), t, "code.ips");
|
||||||
|
path_in(marker, sizeof(marker), t, "mbmnet.txt");
|
||||||
|
path_in(backup, sizeof(backup), t, "code.ips.vor-mbmnet");
|
||||||
|
|
||||||
|
mkdirs(t->dir);
|
||||||
|
// Fremden Patch (z. B. Pretendo) sichern, nicht überschreiben. Immer den aktuellen: Eine ältere Sicherung
|
||||||
|
// (z. B. von vor einem Nimbus-Update) würde sonst beim Zurückschalten veraltete Patches herstellen
|
||||||
|
// (bei Pretendo Fehler 002-0122).
|
||||||
|
if (file_exists(path) && !file_exists(marker)) {
|
||||||
|
remove(backup);
|
||||||
|
rename(path, backup);
|
||||||
|
backedUp = true;
|
||||||
|
}
|
||||||
|
if (!write_ips(path, ips[i], ipsLen[i], changed)) {
|
||||||
|
snprintf(msg, msglen, "Patch konnte nicht auf die SD-Karte geschrieben werden.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
FILE *m = fopen(marker, "w");
|
||||||
|
if (m) {
|
||||||
|
fprintf(m, "MBMNET-Patch aktiv\nServer: %s\n", server);
|
||||||
|
fclose(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (park_conflicting() > 0) *changed = true; // Neustart nötig, damit das http-Modul neu lädt
|
||||||
|
bool splash = install_splash(alreadyOurs) && lumaconfig_enable_splash();
|
||||||
|
lumaconfig_enable_modules();
|
||||||
|
if (appPatched && lumaconfig_enable_game_patching()) *changed = true;
|
||||||
|
|
||||||
|
snprintf(msg, msglen, "Patches geschrieben.%s%s",
|
||||||
|
backedUp ? "\nAlter Patch wurde gesichert." : "",
|
||||||
|
splash ? "\nLuma zeigt beim Start \"MBMNET aktiv\"." : "");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Diagnose ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
static bool has_ci(const char *s, u32 len, const char *word)
|
||||||
|
{
|
||||||
|
size_t wl = strlen(word);
|
||||||
|
for (u32 i = 0; i + wl <= len; i++) {
|
||||||
|
size_t k = 0;
|
||||||
|
while (k < wl && (s[i + k] | 0x20) == word[k]) k++;
|
||||||
|
if (k == wl) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool interesting(const char *s, u32 len)
|
||||||
|
{
|
||||||
|
return has_ci(s, len, "nintendo") || has_ci(s, len, "http") || has_ci(s, len, "nasc")
|
||||||
|
|| has_ci(s, len, "account") || has_ci(s, len, "%s") || has_ci(s, len, ".net");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dump_strings(FILE *f, const u8 *code, u32 size)
|
||||||
|
{
|
||||||
|
int lines = 0, total = 0;
|
||||||
|
char buf[256];
|
||||||
|
|
||||||
|
// ASCII-Texte
|
||||||
|
for (u32 i = 0; i < size;) {
|
||||||
|
u32 j = i;
|
||||||
|
while (j < size && code[j] >= 0x20 && code[j] < 0x7F) j++;
|
||||||
|
u32 len = j - i;
|
||||||
|
if (len >= 6) total++;
|
||||||
|
if (len >= 4 && interesting((const char *)code + i, len) && lines < 400) {
|
||||||
|
fprintf(f, " %06lX %.*s\n", (unsigned long)i, (int)(len > 200 ? 200 : len), (const char *)code + i);
|
||||||
|
lines++;
|
||||||
|
}
|
||||||
|
i = j + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-16-Texte (ein Zeichen + Nullbyte)
|
||||||
|
for (u32 i = 0; i + 1 < size;) {
|
||||||
|
u32 j = i, n = 0;
|
||||||
|
while (j + 1 < size && code[j] >= 0x20 && code[j] < 0x7F && code[j + 1] == 0 && n < sizeof(buf) - 1) {
|
||||||
|
buf[n++] = code[j];
|
||||||
|
j += 2;
|
||||||
|
}
|
||||||
|
if (n >= 6 && interesting(buf, n) && lines < 500) {
|
||||||
|
fprintf(f, " %06lX [UTF-16] %.*s\n", (unsigned long)i, (int)n, buf);
|
||||||
|
lines++;
|
||||||
|
}
|
||||||
|
i = (j > i) ? j + 2 : i + 1;
|
||||||
|
}
|
||||||
|
fprintf(f, " (%d Texte insgesamt, %d passende gezeigt)\n", total, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sucht den laufenden Prozess eines Titels (Luma liefert die Titel-ID über Info-Typ 0x10001)
|
||||||
|
bool patch_find_process(u64 tid, u32 *pidOut)
|
||||||
|
{
|
||||||
|
static u32 pids[0x60];
|
||||||
|
s32 n = 0;
|
||||||
|
if (R_FAILED(svcGetProcessList(&n, pids, 0x60))) return false;
|
||||||
|
for (s32 i = 0; i < n; i++) {
|
||||||
|
Handle h;
|
||||||
|
if (R_FAILED(svcOpenProcess(&h, pids[i]))) continue;
|
||||||
|
s64 t = 0;
|
||||||
|
Result r = svcGetProcessInfo(&t, h, 0x10001);
|
||||||
|
svcCloseHandle(h);
|
||||||
|
if (R_SUCCEEDED(r) && (u64)t == tid) { *pidOut = pids[i]; return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liest aus dem laufenden Modul, welche Adresse dort wirklich im Speicher steht.
|
||||||
|
// So sieht man, ob Luma den Patch beim Laden angewendet hat.
|
||||||
|
static void dump_runtime(FILE *f, const char *label, u64 tid, u32 offset)
|
||||||
|
{
|
||||||
|
u32 pid = 0;
|
||||||
|
if (!patch_find_process(tid, &pid)) { fprintf(f, " %s: Prozess nicht gefunden\n", label); return; }
|
||||||
|
Handle dbg;
|
||||||
|
Result r = svcDebugActiveProcess(&dbg, pid);
|
||||||
|
if (R_FAILED(r)) { fprintf(f, " %s: kein Zugriff (%08lX)\n", label, (unsigned long)r); return; }
|
||||||
|
DebugEventInfo info;
|
||||||
|
while (R_SUCCEEDED(svcGetProcessDebugEvent(&info, dbg)))
|
||||||
|
if (info.flags & 1) svcContinueDebugEvent(dbg, 0);
|
||||||
|
char buf[64] = { 0 };
|
||||||
|
r = svcReadProcessMemory(buf, dbg, 0x100000 + offset, 48);
|
||||||
|
svcCloseHandle(dbg);
|
||||||
|
if (R_FAILED(r)) { fprintf(f, " %s: Lesen fehlgeschlagen (%08lX)\n", label, (unsigned long)r); return; }
|
||||||
|
for (int i = 0; i < 48 && buf[i]; i++) if (buf[i] < 0x20 || buf[i] > 0x7E) buf[i] = '.';
|
||||||
|
fprintf(f, " %s (PID %lu) bei %06lX: %s\n", label, (unsigned long)pid, (unsigned long)offset, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool patch_diagnose(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
mkdirs(APP_DIR);
|
||||||
|
FILE *f = fopen(DIAG_FILE, "w");
|
||||||
|
if (!f) {
|
||||||
|
snprintf(msg, msglen, "Diagnose konnte nicht gespeichert werden.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fprintf(f, "MBMNET-Diagnose (App v%s)\n\n", APP_VERSION);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
const Target *t = &targets[i];
|
||||||
|
u8 *raw = NULL;
|
||||||
|
u32 rawSize = 0;
|
||||||
|
Result rc = read_from_nand(t->tid, &raw, &rawSize);
|
||||||
|
bool fromSd = false;
|
||||||
|
if (R_FAILED(rc)) fromSd = read_from_sd(t->tid, &raw, &rawSize);
|
||||||
|
|
||||||
|
fprintf(f, "== %s-Modul %016llX\n", t->label, t->tid);
|
||||||
|
if (!raw) {
|
||||||
|
fprintf(f, " nicht lesbar (Fehler %08lX)\n\n", (unsigned long)rc);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool compressed = blz_looks_compressed(raw, rawSize);
|
||||||
|
fprintf(f, " Quelle: %s, Größe %lu, komprimiert: %s\n", fromSd ? "SD-Dump" : "NAND",
|
||||||
|
(unsigned long)rawSize, compressed ? "ja" : "nein");
|
||||||
|
free(raw);
|
||||||
|
|
||||||
|
char err[160];
|
||||||
|
u8 *code;
|
||||||
|
u32 size;
|
||||||
|
if (!load_code(t, &code, &size, err, sizeof(err))) {
|
||||||
|
fprintf(f, " Fehler: %s\n\n", err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
fprintf(f, " entpackt: %lu Bytes\n", (unsigned long)size);
|
||||||
|
dump_strings(f, code, size);
|
||||||
|
fprintf(f, "\n");
|
||||||
|
free(code);
|
||||||
|
}
|
||||||
|
// Installierte Patches: liegen sie auf der SD, und was steht drin?
|
||||||
|
fprintf(f, "== Installierte Patches\n");
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
char ips[128];
|
||||||
|
path_in(ips, sizeof(ips), &targets[i], "code.ips");
|
||||||
|
FILE *pf = fopen(ips, "rb");
|
||||||
|
if (!pf) { fprintf(f, " %s: kein Patch (%s)\n", targets[i].label, ips + 5); continue; }
|
||||||
|
static u8 buf[4096];
|
||||||
|
size_t n = fread(buf, 1, sizeof(buf), pf);
|
||||||
|
fclose(pf);
|
||||||
|
fprintf(f, " %s: %s, %lu Bytes\n", targets[i].label, ips + 5, (unsigned long)n);
|
||||||
|
for (size_t o = 5; o + 5 <= n && memcmp(buf + o, "EOF", 3) != 0;) {
|
||||||
|
u32 off = (buf[o] << 16) | (buf[o + 1] << 8) | buf[o + 2];
|
||||||
|
u32 len = (buf[o + 3] << 8) | buf[o + 4];
|
||||||
|
if (o + 5 + len > n) break;
|
||||||
|
fprintf(f, " bei %06lX: %.*s\n", (unsigned long)off, (int)strnlen((char *)buf + o + 5, len), (char *)buf + o + 5);
|
||||||
|
o += 5 + len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
// Alles in /luma/sysmodules (auch fremde Patches, z. B. von Pretendo)
|
||||||
|
fprintf(f, "== Inhalt von /luma/sysmodules\n");
|
||||||
|
DIR *dir = opendir(SYSMODULE_DIR);
|
||||||
|
if (dir) {
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(dir))) {
|
||||||
|
char full[300];
|
||||||
|
struct stat est;
|
||||||
|
snprintf(full, sizeof(full), "%s/%s", SYSMODULE_DIR, e->d_name);
|
||||||
|
if (stat(full, &est) == 0) fprintf(f, " %-40s %8ld Bytes\n", e->d_name, (long)est.st_size);
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
} else {
|
||||||
|
fprintf(f, " (Ordner fehlt)\n");
|
||||||
|
}
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
// Konto-Plätze des Kontodienstes (act)
|
||||||
|
fprintf(f, "== Konto-Plätze\n");
|
||||||
|
nnid_dump(f);
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
// Was steht im ARBEITSSPEICHER der laufenden Module? (zeigt, ob Luma die Patches angewendet hat)
|
||||||
|
fprintf(f, "== Laufende Module (Arbeitsspeicher)\n");
|
||||||
|
dump_runtime(f, "Freunde", 0x0004013000003202ULL, 0x61279);
|
||||||
|
dump_runtime(f, "Konto ", 0x0004013000003802ULL, 0xE564);
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
// Verbindungstest: so, wie die Systemmodule eine Verbindung aufbauen (mit Nintendo-Zertifikaten)
|
||||||
|
fprintf(f, "== Verbindungstest (wie Freunde-/Konto-Modul)\n");
|
||||||
|
httpcContext c;
|
||||||
|
Result r = httpcOpenContext(&c, HTTPC_METHOD_GET, "http://mbmnetapi.mbm.wtf:81/app/version", 1);
|
||||||
|
fprintf(f, " Verbindung öffnen (http) %08lX\n", (unsigned long)r);
|
||||||
|
if (R_SUCCEEDED(r)) {
|
||||||
|
fprintf(f, " Nintendo-Root-Zertifikat %08lX\n", (unsigned long)httpcAddDefaultCert(&c, SSLC_DefaultRootCert_Nintendo_CA_G3));
|
||||||
|
fprintf(f, " Client-Zertifikat %08lX\n", (unsigned long)httpcSetClientCertDefault(&c, SSLC_DefaultClientCert_ClCertA));
|
||||||
|
r = httpcBeginRequest(&c);
|
||||||
|
u32 status = 0;
|
||||||
|
if (R_SUCCEEDED(r)) httpcGetResponseStatusCodeTimeout(&c, &status, 10ULL * 1000 * 1000 * 1000);
|
||||||
|
fprintf(f, " Anfrage senden %08lX HTTP %lu\n", (unsigned long)r, (unsigned long)status);
|
||||||
|
httpcCloseContext(&c);
|
||||||
|
}
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
// Boot-Splash und Luma-Einstellungen
|
||||||
|
fprintf(f, "== Boot-Splash / Luma-Einstellungen\n");
|
||||||
|
struct stat st;
|
||||||
|
if (stat(SPLASH_FILE, &st) == 0)
|
||||||
|
fprintf(f, " /luma/splash.bin: %ld Bytes%s\n", (long)st.st_size, st.st_size == SPLASH_SIZE ? " (richtig)" : " (FALSCHE GRÖSSE, Luma erwartet 288000)");
|
||||||
|
else
|
||||||
|
fprintf(f, " /luma/splash.bin: fehlt\n");
|
||||||
|
FILE *cfg = fopen("sdmc:/luma/config.ini", "r");
|
||||||
|
if (cfg) {
|
||||||
|
char line[160];
|
||||||
|
while (fgets(line, sizeof(line), cfg)) {
|
||||||
|
if (strstr(line, "splash_position") || strstr(line, "splash_duration") || strstr(line, "config_version")
|
||||||
|
|| strstr(line, "enable_external_firm_and_modules ="))
|
||||||
|
fprintf(f, " config.ini: %s", line);
|
||||||
|
}
|
||||||
|
fclose(cfg);
|
||||||
|
} else {
|
||||||
|
fprintf(f, " /luma/config.ini: fehlt (ältere Luma-Version? Splash dann im Luma-Menü einschalten)\n");
|
||||||
|
}
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
// Kontospeicher: Darf die App Systemspeicher öffnen? Scheitert nur der eines laufenden Dienstes
|
||||||
|
// (act, frd), ist er belegt; scheitern alle, fehlt eine Berechtigung.
|
||||||
|
fprintf(f, "== Systemspeicher öffnen (für \"Anmeldung merken\")\n");
|
||||||
|
static const struct { u32 id; const char *name; } saves[] = {
|
||||||
|
{ 0x00010038, "act (Kontodienst, läuft)" },
|
||||||
|
{ 0x00010032, "frd (Freunde-Dienst, läuft)" },
|
||||||
|
{ 0x00010017, "cfg (Systemeinstellungen-Modul, läuft)" },
|
||||||
|
{ 0x00020082, "HOME-Menü (System-App)" },
|
||||||
|
{ 0x00020086, "Systemeinstellungen (System-App, läuft nicht)" },
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(saves) / sizeof(saves[0]); i++) {
|
||||||
|
u32 path[2] = { MEDIATYPE_NAND, saves[i].id };
|
||||||
|
FS_Archive arch;
|
||||||
|
Result r = FSUSER_OpenArchive(&arch, ARCHIVE_SYSTEM_SAVEDATA, (FS_Path){ PATH_BINARY, sizeof(path), path });
|
||||||
|
int files = -1;
|
||||||
|
if (R_SUCCEEDED(r)) {
|
||||||
|
Handle dir;
|
||||||
|
if (R_SUCCEEDED(FSUSER_OpenDirectory(&dir, arch, fsMakePath(PATH_ASCII, "/")))) {
|
||||||
|
FS_DirectoryEntry e[16];
|
||||||
|
u32 n = 0;
|
||||||
|
files = R_SUCCEEDED(FSDIR_Read(dir, &n, 16, e)) ? (int)n : -2;
|
||||||
|
FSDIR_Close(dir);
|
||||||
|
}
|
||||||
|
FSUSER_CloseArchive(arch);
|
||||||
|
}
|
||||||
|
fprintf(f, " %08lX %-44s %08lX%s", (unsigned long)saves[i].id, saves[i].name, (unsigned long)r, R_SUCCEEDED(r) ? " OK" : "");
|
||||||
|
if (files >= 0) fprintf(f, ", %d Einträge", files);
|
||||||
|
fprintf(f, "\n");
|
||||||
|
}
|
||||||
|
fprintf(f, " Als %s gestartet\n", envIsHomebrew() ? "3dsx (Homebrew Launcher)" : "CIA");
|
||||||
|
fprintf(f, "\n");
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
snprintf(msg, msglen, "Diagnose gespeichert:\n/3ds/mbmnet/diagnose.txt\nBitte an den Entwickler schicken.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool patch_disable(char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
PatchState st = patch_state();
|
||||||
|
if (st != STATE_MBMNET) {
|
||||||
|
snprintf(msg, msglen, st == STATE_FOREIGN
|
||||||
|
? "MBMNET ist nicht aktiv.\nDer vorhandene fremde Patch bleibt unverändert."
|
||||||
|
: "Schon im Originalzustand.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_splash();
|
||||||
|
restore_conflicting();
|
||||||
|
lumaconfig_restore_splash();
|
||||||
|
lumaconfig_restore_modules();
|
||||||
|
lumaconfig_restore_game_patching();
|
||||||
|
|
||||||
|
bool restored = false;
|
||||||
|
for (size_t i = 0; i < TARGET_COUNT; i++) {
|
||||||
|
const Target *t = &targets[i];
|
||||||
|
char ips[128], marker[128], backup[128];
|
||||||
|
path_in(ips, sizeof(ips), t, "code.ips");
|
||||||
|
path_in(marker, sizeof(marker), t, "mbmnet.txt");
|
||||||
|
path_in(backup, sizeof(backup), t, "code.ips.vor-mbmnet");
|
||||||
|
|
||||||
|
if (!file_exists(marker)) continue;
|
||||||
|
remove(ips);
|
||||||
|
remove(marker);
|
||||||
|
if (file_exists(backup)) {
|
||||||
|
rename(backup, ips);
|
||||||
|
restored = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(msg, msglen, restored ? "Vorheriger Patch wiederhergestellt." : "Patches entfernt.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
// Offizieller MBMNET-Server. Über "Server-Adresse" lässt sich ein anderer eintragen (z. B. zum Testen).
|
||||||
|
// Fester MBMNET-Server: einfaches HTTP über den NetBird-TCP-Service auf Port 81.
|
||||||
|
// Höchstens 21 Zeichen passen in den Patch.
|
||||||
|
#define MBMNET_SERVER "mbmnetapi.mbm.wtf:81"
|
||||||
|
|
||||||
|
#define APP_DIR "sdmc:/3ds/mbmnet"
|
||||||
|
#define DIAG_FILE APP_DIR "/diagnose.txt"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
STATE_ORIGINAL, // kein Patch: Nintendo-Originalzustand
|
||||||
|
STATE_MBMNET, // unser Patch ist aktiv
|
||||||
|
STATE_FOREIGN, // fremder Patch (z. B. Pretendo/Nimbus) ist aktiv
|
||||||
|
} PatchState;
|
||||||
|
|
||||||
|
PatchState patch_state(void);
|
||||||
|
// Räumt Patches alter App-Versionen (/luma/titles/...) weg. true, wenn etwas entfernt wurde.
|
||||||
|
bool patch_migrate(void);
|
||||||
|
|
||||||
|
// Erzeugt die Patches (Freunde + Konto) und aktiviert MBMNET. msg bekommt eine lesbare Meldung.
|
||||||
|
// *changed: true, wenn sich Patch-Dateien geändert haben (dann ist ein Neustart nötig).
|
||||||
|
bool patch_enable(const char *server, bool *changed, char *msg, size_t msglen);
|
||||||
|
// Schreibt alle Adress-Texte aus den Modulen nach DIAG_FILE (zur Fehlersuche).
|
||||||
|
bool patch_diagnose(char *msg, size_t msglen);
|
||||||
|
// Entfernt unsere Patches und stellt eventuell gesicherte fremde Patches wieder her.
|
||||||
|
bool patch_disable(char *msg, size_t msglen);
|
||||||
|
// Läuft ein Prozess mit dieser Titel-ID? (über Lumas Prozess-Info), PID in pidOut
|
||||||
|
bool patch_find_process(u64 tid, u32 *pidOut);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Startet die App nach einem Update selbst neu.
|
||||||
|
//
|
||||||
|
// CIA: aptSetChainloaderToSelf() - das System startet beim Beenden denselben Titel
|
||||||
|
// (jetzt die neue Version) wieder.
|
||||||
|
// 3DSX: Die App läuft im Homebrew-Titel von Luma (Rosalina). Über dessen Dienst "hb:ldr"
|
||||||
|
// legen wir fest, welche .3dsx beim nächsten Start geladen wird - genau so macht es
|
||||||
|
// auch der Homebrew Launcher. Danach den Homebrew-Titel neu starten lassen.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "relaunch.h"
|
||||||
|
|
||||||
|
static Result hbldr_call(Handle h, u32 cmd, const void *buf, u32 size, int staticId)
|
||||||
|
{
|
||||||
|
u32 *cmdbuf = getThreadCommandBuffer();
|
||||||
|
cmdbuf[0] = IPC_MakeHeader(cmd, 0, 2);
|
||||||
|
cmdbuf[1] = IPC_Desc_StaticBuffer(size, staticId);
|
||||||
|
cmdbuf[2] = (u32)buf;
|
||||||
|
Result rc = svcSendSyncRequest(h);
|
||||||
|
return R_SUCCEEDED(rc) ? (Result)cmdbuf[1] : rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool set_homebrew_target(const char *selfPath)
|
||||||
|
{
|
||||||
|
Handle h;
|
||||||
|
if (R_FAILED(svcConnectToPort(&h, "hb:ldr"))) return false;
|
||||||
|
|
||||||
|
// hb:ldr erwartet den Pfad ohne "sdmc:"
|
||||||
|
const char *path = strncmp(selfPath, "sdmc:/", 6) == 0 ? selfPath + 5 : selfPath;
|
||||||
|
Result rc = hbldr_call(h, 2, path, strlen(path) + 1, 0); // SetTarget
|
||||||
|
|
||||||
|
// argv wie beim Homebrew Launcher: u32 Anzahl, dann die Texte
|
||||||
|
static u32 argBuf[0x400 / 4];
|
||||||
|
memset(argBuf, 0, sizeof(argBuf));
|
||||||
|
argBuf[0] = 1;
|
||||||
|
strncpy((char *)&argBuf[1], selfPath, sizeof(argBuf) - 5);
|
||||||
|
if (R_SUCCEEDED(rc)) rc = hbldr_call(h, 3, argBuf, sizeof(argBuf), 1); // SetArgv
|
||||||
|
|
||||||
|
svcCloseHandle(h);
|
||||||
|
return R_SUCCEEDED(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool relaunch_prepare(const char *selfPath)
|
||||||
|
{
|
||||||
|
if (envIsHomebrew() && !set_homebrew_target(selfPath)) return false;
|
||||||
|
aptSetChainloaderToSelf();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
|
||||||
|
// Bereitet den Neustart der App vor. Danach main() verlassen, dann startet das System
|
||||||
|
// die (neue) App wieder. false: Neustart nicht möglich, Nutzer muss selbst neu starten.
|
||||||
|
bool relaunch_prepare(const char *selfPath);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,249 @@
|
|||||||
|
// SpotPass (BOSS) für MBMNET-Mitteilungen, siehe spotpass.h.
|
||||||
|
//
|
||||||
|
// Wie in der libctru-Vorlage (examples/network/boss): eigene Aufgabe mit HTTP-URL. Der Server liefert einen
|
||||||
|
// BOSS-Container, dessen Inhalt an das News-Modul (0004013000003502) geht. Die Nintendo-Signatur darin ist
|
||||||
|
// leer, Luma schaltet ihre Prüfung ab.
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "spotpass.h"
|
||||||
|
|
||||||
|
#define TASK_NEWS "mbmnews"
|
||||||
|
#define TASK_TEST "mbmtest"
|
||||||
|
#define NEWS_INTERVAL (2 * 60) // Sekunden zwischen zwei Abrufen
|
||||||
|
|
||||||
|
// SpotPass braucht für jede Anwendung mit Aufgaben einen angemeldeten Speicher (Extdata), wie bei Spielen.
|
||||||
|
// Die Mitteilungen selbst landen im News-Modul, der Speicher bleibt praktisch leer.
|
||||||
|
#define EXTDATA_ID 0xFB4E0ULL // = Unique ID der App (cia.rsf: ExtSaveDataId)
|
||||||
|
#define BOSS_STORAGE 0x100000 // 1 MB
|
||||||
|
|
||||||
|
// Policy-Liste: SpotPass lädt sie vor jeder Aufgabe von Nintendo (nppl.c.app.nintendowifi.net). Der Server
|
||||||
|
// antwortet nicht mehr, ohne Liste bricht jede Aufgabe ab. Darum auf MBMNET umstellen (wie Pretendo).
|
||||||
|
// boss:P-Befehl 0x0427 SetPolicyListUrl, leerer Text = wieder Nintendos Adresse. Klappt nur mit boss:P.
|
||||||
|
static Result set_policy_url(const char *url)
|
||||||
|
{
|
||||||
|
u32 size = strlen(url) + 1;
|
||||||
|
u32 *cmdbuf = getThreadCommandBuffer();
|
||||||
|
cmdbuf[0] = IPC_MakeHeader(0x427, 1, 2); // 0x04270042
|
||||||
|
cmdbuf[1] = size;
|
||||||
|
cmdbuf[2] = IPC_Desc_Buffer(size, IPC_BUFFER_R);
|
||||||
|
cmdbuf[3] = (u32)url;
|
||||||
|
Result rc = svcSendSyncRequest(bossGetSessionHandle());
|
||||||
|
return R_SUCCEEDED(rc) ? (Result)cmdbuf[1] : rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result use_mbmnet_policy(const char *server)
|
||||||
|
{
|
||||||
|
char url[128];
|
||||||
|
snprintf(url, sizeof(url), "http://%s/p01/policylist", server);
|
||||||
|
return set_policy_url(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gibt es die Extdata? Sonst anlegen (mit Ordner /boss, in den SpotPass schreibt). bossSetStorageInfo
|
||||||
|
// prüft das nicht - ohne Extdata scheitert erst die Aufgabe (Fehler 004-6006).
|
||||||
|
static Result ensure_extdata(void)
|
||||||
|
{
|
||||||
|
u32 lowHigh[3] = { MEDIATYPE_SD, (u32)EXTDATA_ID, (u32)(EXTDATA_ID >> 32) };
|
||||||
|
FS_Path path = { PATH_BINARY, sizeof(lowHigh), lowHigh };
|
||||||
|
FS_Archive arch;
|
||||||
|
Result rc = fsInit();
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
rc = FSUSER_OpenArchive(&arch, ARCHIVE_EXTDATA, path);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
FS_ExtSaveDataInfo info = { .mediaType = MEDIATYPE_SD, .saveId = EXTDATA_ID };
|
||||||
|
rc = FSUSER_CreateExtSaveData(info, 32, 256, 0, 0, NULL);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
// Manche Systemversionen wollen ein Icon (SMDH) dazu: leeres mit Kennung reicht
|
||||||
|
static u8 smdh[0x36C0];
|
||||||
|
memcpy(smdh, "SMDH", 4);
|
||||||
|
rc = FSUSER_CreateExtSaveData(info, 32, 256, 0, sizeof(smdh), smdh);
|
||||||
|
}
|
||||||
|
if (R_SUCCEEDED(rc)) rc = FSUSER_OpenArchive(&arch, ARCHIVE_EXTDATA, path);
|
||||||
|
}
|
||||||
|
if (R_SUCCEEDED(rc)) {
|
||||||
|
FSUSER_CreateDirectory(arch, fsMakePath(PATH_ASCII, "/boss"), 0); // gibt es ihn schon: egal
|
||||||
|
FSUSER_CloseArchive(arch);
|
||||||
|
}
|
||||||
|
fsExit();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result ensure_storage(void)
|
||||||
|
{
|
||||||
|
return bossSetStorageInfo(EXTDATA_ID, BOSS_STORAGE, MEDIATYPE_SD);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool url_for(const char *server, const char *file, char *url, size_t len)
|
||||||
|
{
|
||||||
|
u32 device = 0;
|
||||||
|
bool ok = R_SUCCEEDED(amInit());
|
||||||
|
if (ok) {
|
||||||
|
s32 internal = 0;
|
||||||
|
ok = R_SUCCEEDED(AM_GetDeviceId(&internal, &device)) && internal >= 0 && device;
|
||||||
|
amExit();
|
||||||
|
}
|
||||||
|
if (ok) snprintf(url, len, "http://%s/spotpass/%lu/%s", server, (unsigned long)device, file);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// step bekommt den Namen des Schritts, der fehlschlug
|
||||||
|
static Result setup_task(const char *server, const char *task, const char *url, u32 interval, const char **step)
|
||||||
|
{
|
||||||
|
bool once = strcmp(task, TASK_TEST) == 0;
|
||||||
|
*step = "Policy-Liste umstellen";
|
||||||
|
Result rc = use_mbmnet_policy(server);
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
*step = "Speicher anlegen";
|
||||||
|
rc = ensure_extdata();
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
*step = "Speicher anmelden";
|
||||||
|
rc = ensure_storage();
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
bossDeleteTask(task, 0);
|
||||||
|
bossContext ctx;
|
||||||
|
bossSetupContextDefault(&ctx, interval, url);
|
||||||
|
ctx.property[0x4] = once ? 1 : 0; // Count: 0 = dauerhaft (Standard wären 100 Läufe), Test nur einmal
|
||||||
|
*step = "Einstellungen senden";
|
||||||
|
rc = bossSendContextConfig(&ctx);
|
||||||
|
if (R_FAILED(rc)) return rc;
|
||||||
|
*step = "Aufgabe anmelden";
|
||||||
|
return bossRegisterTask(task, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool spotpass_register(const char *server, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
char url[256];
|
||||||
|
if (!url_for(server, "news.boss", url, sizeof(url))) {
|
||||||
|
snprintf(msg, msglen, "SpotPass: Geräte-ID nicht lesbar");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char *step = "BOSS starten";
|
||||||
|
Result rc = bossInit(0, false);
|
||||||
|
if (R_SUCCEEDED(rc)) {
|
||||||
|
rc = setup_task(server, TASK_NEWS, url, NEWS_INTERVAL, &step);
|
||||||
|
if (R_SUCCEEDED(rc)) bossStartBgImmediate(TASK_NEWS); // gleich einmal abholen
|
||||||
|
bossExit();
|
||||||
|
}
|
||||||
|
if (R_FAILED(rc)) snprintf(msg, msglen, "SpotPass anmelden fehlgeschlagen bei: %s (%08lX)", step, (unsigned long)rc);
|
||||||
|
return R_SUCCEEDED(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void spotpass_unregister(void)
|
||||||
|
{
|
||||||
|
if (R_FAILED(bossInit(0, false))) return;
|
||||||
|
bossDeleteTask(TASK_NEWS, 0);
|
||||||
|
set_policy_url(""); // wieder Nintendos Policy-Liste
|
||||||
|
bossExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Fehlerauswertung (BOSSU-Befehle, die libctru nicht hat) ---------------------------------------
|
||||||
|
static Result boss_send(u32 *cmdbuf)
|
||||||
|
{
|
||||||
|
Result rc = svcSendSyncRequest(bossGetSessionHandle());
|
||||||
|
return R_SUCCEEDED(rc) ? (Result)cmdbuf[1] : rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Befehl mit Task-ID-Puffer; extra = Werte zwischen Größe und Puffer
|
||||||
|
static Result boss_task_cmd(u16 id, const char *task, int nextra, u32 e1, u32 e2, u32 *out)
|
||||||
|
{
|
||||||
|
u32 size = strlen(task) + 1;
|
||||||
|
u32 *cmd = getThreadCommandBuffer();
|
||||||
|
cmd[0] = IPC_MakeHeader(id, 1 + nextra, 2);
|
||||||
|
cmd[1] = size;
|
||||||
|
if (nextra > 0) cmd[2] = e1;
|
||||||
|
if (nextra > 1) cmd[3] = e2;
|
||||||
|
cmd[2 + nextra] = IPC_Desc_Buffer(size, IPC_BUFFER_R);
|
||||||
|
cmd[3 + nextra] = (u32)task;
|
||||||
|
Result rc = boss_send(cmd);
|
||||||
|
if (out) *out = cmd[2];
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Result boss_receive_property(u16 property, void *buf, u32 size)
|
||||||
|
{
|
||||||
|
u32 *cmd = getThreadCommandBuffer();
|
||||||
|
cmd[0] = IPC_MakeHeader(0x16, 2, 2); // ReceiveProperty
|
||||||
|
cmd[1] = property;
|
||||||
|
cmd[2] = size;
|
||||||
|
cmd[3] = IPC_Desc_Buffer(size, IPC_BUFFER_W);
|
||||||
|
cmd[4] = (u32)buf;
|
||||||
|
return boss_send(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fehler einer Aufgabe als Text: Nintendo-Fehlercode 004-XXXX, Ergebnis, Dienststatus, HTTP-Code
|
||||||
|
static void task_error_text(const char *task, char *out, size_t len)
|
||||||
|
{
|
||||||
|
u8 result = 0, service = 0, taskErr = 0;
|
||||||
|
u32 http = 0, code = 0;
|
||||||
|
boss_task_cmd(0x23, task, 2, 0, 0, NULL); // GetTaskStatus: lädt die Status-Eigenschaften
|
||||||
|
boss_receive_property(0x20, &result, 1); // TaskResultCode
|
||||||
|
boss_receive_property(0x21, &service, 1); // TaskServiceStatus
|
||||||
|
boss_task_cmd(0x24, task, 1, 0, 0, NULL); // GetTaskError: lädt Eigenschaft 0x30
|
||||||
|
boss_receive_property(0x30, &taskErr, 1);
|
||||||
|
boss_task_cmd(0x22, task, 0, 0, 0, &http); // GetTaskCommErrorCode (HTTP-Status)
|
||||||
|
u32 *cmd = getThreadCommandBuffer();
|
||||||
|
cmd[0] = IPC_MakeHeader(0x2E, 1, 0); // GetErrorCode: Ergebnis -> 004-XXXX
|
||||||
|
cmd[1] = result;
|
||||||
|
if (R_SUCCEEDED(boss_send(cmd))) code = cmd[2];
|
||||||
|
snprintf(out, len, "Fehler %03lu-%04lu (Ergebnis 0x%02X, Dienst 0x%02X, Aufgabe 0x%02X, HTTP %lu)",
|
||||||
|
(unsigned long)(code / 10000), (unsigned long)(code % 10000), result, service, taskErr, (unsigned long)http);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char testUrl[256];
|
||||||
|
static bool testSeenRunning;
|
||||||
|
|
||||||
|
bool spotpass_test_start(const char *server, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (!url_for(server, "test.boss", testUrl, sizeof(testUrl))) {
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test: Geräte-ID nicht lesbar");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Result rc = bossInit(0, false);
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test: BOSS nicht verfügbar (%08lX)", (unsigned long)rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const char *step = "";
|
||||||
|
rc = setup_task(server, TASK_TEST, testUrl, 60, &step);
|
||||||
|
if (R_SUCCEEDED(rc)) { step = "Aufgabe starten"; rc = bossStartTaskImmediate(TASK_TEST); }
|
||||||
|
bossExit();
|
||||||
|
if (R_FAILED(rc)) {
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test fehlgeschlagen bei: %s (%08lX)", step, (unsigned long)rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
testSeenRunning = false;
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test gestartet ...");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool spotpass_test_poll(int seconds, char *msg, size_t msglen)
|
||||||
|
{
|
||||||
|
if (R_FAILED(bossInit(0, false))) return true;
|
||||||
|
u8 status = 0, out2 = 0;
|
||||||
|
u32 out1 = 0;
|
||||||
|
Result rc = bossGetTaskState(TASK_TEST, 0, &status, &out1, &out2);
|
||||||
|
bool running = R_SUCCEEDED(rc) && status != BOSSTASKSTATUS_ERROR && !(testSeenRunning && status != BOSSTASKSTATUS_STARTED);
|
||||||
|
if (status == BOSSTASKSTATUS_STARTED) testSeenRunning = true;
|
||||||
|
if (running && seconds < 180) {
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test läuft ... %d s (Status %u)", seconds, status);
|
||||||
|
bossExit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (R_FAILED(rc))
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test: Status nicht lesbar (%08lX)", (unsigned long)rc);
|
||||||
|
else if (running)
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test nach 3 Min. noch nicht fertig (Status %u).\n"
|
||||||
|
"Er läuft im Hintergrund weiter. Später in die Mitteilungen schauen.", status);
|
||||||
|
else if (status == BOSSTASKSTATUS_ERROR) {
|
||||||
|
char why[128];
|
||||||
|
task_error_text(TASK_TEST, why, sizeof(why));
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test: Abruf fehlgeschlagen.\n%s\n%s", why, testUrl);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
snprintf(msg, msglen, "SpotPass-Test abgeholt (Status %u, %lu, %u).\nSteht \"MBMNET SpotPass-Test\" in den Mitteilungen?",
|
||||||
|
status, (unsigned long)out1, out2);
|
||||||
|
if (!running) bossDeleteTask(TASK_TEST, 0); // nur löschen, wenn wirklich fertig
|
||||||
|
bossExit();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// SpotPass (BOSS): Der 3DS holt MBMNET-Nachrichten und Ankündigungen selbst im Hintergrund ab (auch im
|
||||||
|
// Standby, ohne App) und zeigt sie als Mitteilung im HOME-Menü. Die Aufgabe ruft alle 2 Minuten
|
||||||
|
// http://<server>/spotpass/<Geräte-ID>/news.boss ab (MBMNET Accounting, spotpass.js).
|
||||||
|
|
||||||
|
// Hintergrund-Aufgabe anmelden (bei aktivem MBMNET). false + msg bei Fehler.
|
||||||
|
bool spotpass_register(const char *server, char *msg, size_t msglen);
|
||||||
|
// Hintergrund-Aufgabe abmelden (beim Wechsel zu Nintendo)
|
||||||
|
void spotpass_unregister(void);
|
||||||
|
// Test für die Werkzeuge: startet den sofortigen Abruf einer Test-Mitteilung. true = läuft,
|
||||||
|
// danach jede Sekunde spotpass_test_poll aufrufen.
|
||||||
|
bool spotpass_test_start(const char *server, char *msg, size_t msglen);
|
||||||
|
// true = Test noch nicht fertig. msg bekommt den Stand bzw. das Ergebnis.
|
||||||
|
bool spotpass_test_poll(int seconds, char *msg, size_t msglen);
|
||||||
+277
@@ -0,0 +1,277 @@
|
|||||||
|
// Auto-Update. Die App fragt den MBMNET-Server (per HTTP, das kann der 3DS zuverlässig):
|
||||||
|
// GET /app/version -> z. B. "1.2.0"
|
||||||
|
// GET /app/mbmnet.3dsx -> die neue App (Homebrew-Launcher-Version)
|
||||||
|
// GET /app/mbmnet.cia -> die neue App (installierte CIA-Version)
|
||||||
|
// Der Server holt beides von deinem Git-Server (siehe config.json / README).
|
||||||
|
#include <3ds.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "update.h"
|
||||||
|
|
||||||
|
#define MAX_DOWNLOAD (8 * 1024 * 1024)
|
||||||
|
|
||||||
|
static volatile UpdateState state = UPD_IDLE;
|
||||||
|
static char server[64];
|
||||||
|
static char selfPath[256];
|
||||||
|
static char newVersion[32];
|
||||||
|
static Thread thread;
|
||||||
|
static bool runningAsCia;
|
||||||
|
|
||||||
|
// Fehlersuche: Schritt und Code des letzten Fehlers, dazu ein Protokoll auf der SD
|
||||||
|
#define LOG_FILE "sdmc:/3ds/mbmnet/update.log"
|
||||||
|
static char errStep[48];
|
||||||
|
static Result errCode;
|
||||||
|
static volatile int downloadAttempt; // laufender Versuch (für die Anzeige)
|
||||||
|
|
||||||
|
static void ulog(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(LOG_FILE, "a");
|
||||||
|
if (!f) return;
|
||||||
|
va_list ap;
|
||||||
|
va_start(ap, fmt);
|
||||||
|
vfprintf(f, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
fputc('\n', f);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merkt sich den Fehler. Den Zustand setzt erst der Worker, wenn alle Versuche erfolglos waren.
|
||||||
|
static void fail(const char *stepName, Result rc)
|
||||||
|
{
|
||||||
|
snprintf(errStep, sizeof(errStep), "%s", stepName);
|
||||||
|
errCode = rc;
|
||||||
|
ulog("FEHLER bei %s: %08lX", stepName, (unsigned long)rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ATTEMPTS 3
|
||||||
|
static void pause_seconds(int s) { svcSleepThread((s64)s * 1000 * 1000 * 1000); }
|
||||||
|
|
||||||
|
int update_attempt(void) { return downloadAttempt; }
|
||||||
|
|
||||||
|
void update_error(char *out, size_t len)
|
||||||
|
{
|
||||||
|
snprintf(out, len, "%s (Fehler %08lX)", errStep[0] ? errStep : "?", (unsigned long)errCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateState update_state(void) { return state; }
|
||||||
|
const char *update_new_version(void) { return newVersion; }
|
||||||
|
|
||||||
|
// Lädt eine URL komplett in den Speicher. Gibt den HTTP-Status zurück (0 bei Netzwerkfehler).
|
||||||
|
static Result lastHttpRc; // Fehlercode, falls http_get 0 zurückgibt
|
||||||
|
|
||||||
|
static u32 http_get(const char *url, u8 **out, u32 *outSize)
|
||||||
|
{
|
||||||
|
httpcContext ctx;
|
||||||
|
u32 status = 0;
|
||||||
|
*out = NULL;
|
||||||
|
*outSize = 0;
|
||||||
|
|
||||||
|
lastHttpRc = httpcOpenContext(&ctx, HTTPC_METHOD_GET, url, 1);
|
||||||
|
if (R_FAILED(lastHttpRc)) return 0;
|
||||||
|
httpcAddRequestHeaderField(&ctx, "User-Agent", "MBMNET-Switcher/" APP_VERSION);
|
||||||
|
httpcSetKeepAlive(&ctx, HTTPC_KEEPALIVE_DISABLED);
|
||||||
|
|
||||||
|
if (R_SUCCEEDED(lastHttpRc = httpcBeginRequest(&ctx)) &&
|
||||||
|
R_SUCCEEDED(lastHttpRc = httpcGetResponseStatusCodeTimeout(&ctx, &status, 10ULL * 1000 * 1000 * 1000)) &&
|
||||||
|
status == 200) {
|
||||||
|
u32 cap = 64 * 1024, size = 0, got;
|
||||||
|
u8 *buf = malloc(cap);
|
||||||
|
Result rc;
|
||||||
|
do {
|
||||||
|
if (!buf) break;
|
||||||
|
if (size == cap) {
|
||||||
|
if (cap >= MAX_DOWNLOAD) { free(buf); buf = NULL; break; }
|
||||||
|
u8 *bigger = realloc(buf, cap * 2);
|
||||||
|
if (!bigger) { free(buf); buf = NULL; break; }
|
||||||
|
buf = bigger;
|
||||||
|
cap *= 2;
|
||||||
|
}
|
||||||
|
got = 0;
|
||||||
|
rc = httpcDownloadData(&ctx, buf + size, cap - size, &got);
|
||||||
|
size += got;
|
||||||
|
} while (rc == (Result)HTTPC_RESULTCODE_DOWNLOADPENDING);
|
||||||
|
|
||||||
|
if (buf && R_SUCCEEDED(rc)) {
|
||||||
|
*out = buf;
|
||||||
|
*outSize = size;
|
||||||
|
} else {
|
||||||
|
lastHttpRc = buf ? rc : -2; // -2 = zu wenig Speicher
|
||||||
|
*outSize = size;
|
||||||
|
free(buf);
|
||||||
|
status = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
httpcCloseContext(&ctx);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installiert eine CIA über den Systemdienst am:net (wie FBI)
|
||||||
|
static bool install_cia(const u8 *data, u32 size)
|
||||||
|
{
|
||||||
|
Handle h;
|
||||||
|
Result rc = AM_StartCiaInstall(MEDIATYPE_SD, &h);
|
||||||
|
ulog("CIA-Installation starten: %08lX", (unsigned long)rc);
|
||||||
|
if (R_FAILED(rc)) { fail("CIA-Installation starten", rc); return false; }
|
||||||
|
for (u32 off = 0; off < size;) {
|
||||||
|
u32 chunk = size - off > 0x10000 ? 0x10000 : size - off;
|
||||||
|
u32 written = 0;
|
||||||
|
rc = FSFILE_Write(h, &written, off, data + off, chunk, 0);
|
||||||
|
if (R_FAILED(rc) || written != chunk) {
|
||||||
|
ulog("Schreiben bei %lu: %08lX (%lu von %lu Bytes)", (unsigned long)off, (unsigned long)rc,
|
||||||
|
(unsigned long)written, (unsigned long)chunk);
|
||||||
|
AM_CancelCIAInstall(h);
|
||||||
|
fail("CIA schreiben", rc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
off += chunk;
|
||||||
|
}
|
||||||
|
rc = AM_FinishCiaInstall(h);
|
||||||
|
ulog("CIA-Installation abschließen: %08lX", (unsigned long)rc);
|
||||||
|
if (R_FAILED(rc)) { fail("CIA-Installation abschließen", rc); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schreibt die neue .3dsx über die laufende
|
||||||
|
static bool install_3dsx(const u8 *data, u32 size)
|
||||||
|
{
|
||||||
|
char tmpPath[sizeof(selfPath) + 8];
|
||||||
|
snprintf(tmpPath, sizeof(tmpPath), "%s.neu", selfPath);
|
||||||
|
FILE *f = fopen(tmpPath, "wb");
|
||||||
|
bool ok = f && fwrite(data, 1, size, f) == size;
|
||||||
|
if (f) fclose(f);
|
||||||
|
ulog("3DSX schreiben nach %s: %s", tmpPath, ok ? "OK" : "FEHLER");
|
||||||
|
if (!ok) { fail("3DSX schreiben", -1); return false; }
|
||||||
|
// Erst komplett schreiben, dann austauschen: so bleibt bei einem Abbruch die alte App heil.
|
||||||
|
remove(selfPath);
|
||||||
|
ok = rename(tmpPath, selfPath) == 0;
|
||||||
|
ulog("3DSX austauschen (%s): %s", selfPath, ok ? "OK" : "FEHLER");
|
||||||
|
if (!ok) fail("3DSX austauschen", -1);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool is_newer(const char *remote, const char *local)
|
||||||
|
{
|
||||||
|
int r[3] = { 0 }, l[3] = { 0 };
|
||||||
|
sscanf(remote, "%d.%d.%d", &r[0], &r[1], &r[2]);
|
||||||
|
sscanf(local, "%d.%d.%d", &l[0], &l[1], &l[2]);
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (r[i] != l[i]) return r[i] > l[i];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schritt 1: gibt es eine neuere Version? (bei Netzwerkfehlern bis zu 3 Versuche)
|
||||||
|
static void check_worker(void *arg)
|
||||||
|
{
|
||||||
|
char url[128];
|
||||||
|
u8 *data;
|
||||||
|
u32 size;
|
||||||
|
|
||||||
|
snprintf(url, sizeof(url), "http://%s/app/version", server);
|
||||||
|
for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
||||||
|
u32 status = http_get(url, &data, &size);
|
||||||
|
if (status == 404) { state = UPD_NONE; return; }
|
||||||
|
if (status == 200) {
|
||||||
|
snprintf(newVersion, sizeof(newVersion), "%.*s", (int)(size < 31 ? size : 31), (char *)data);
|
||||||
|
newVersion[strcspn(newVersion, " \r\n")] = 0;
|
||||||
|
if (newVersion[0] == 'v') memmove(newVersion, newVersion + 1, strlen(newVersion));
|
||||||
|
free(data);
|
||||||
|
state = is_newer(newVersion, APP_VERSION) ? UPD_AVAILABLE : UPD_LATEST;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
free(data);
|
||||||
|
if (attempt < ATTEMPTS) pause_seconds(2);
|
||||||
|
}
|
||||||
|
state = UPD_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein Versuch: laden, prüfen, installieren
|
||||||
|
static bool download_and_install(const char *url)
|
||||||
|
{
|
||||||
|
u8 *data;
|
||||||
|
u32 size;
|
||||||
|
u32 status = http_get(url, &data, &size);
|
||||||
|
ulog("Download: HTTP %lu, %lu Bytes", (unsigned long)status, (unsigned long)size);
|
||||||
|
if (status != 200) {
|
||||||
|
free(data);
|
||||||
|
fail(status ? "Herunterladen (HTTP-Status)" : "Herunterladen", status ? (Result)status : lastHttpRc);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nur echte Dateien installieren: CIA beginnt mit Headergröße 0x2020, 3DSX mit "3DSX"
|
||||||
|
bool valid = size >= 32 && (runningAsCia
|
||||||
|
? data[0] == 0x20 && data[1] == 0x20 && data[2] == 0 && data[3] == 0
|
||||||
|
: memcmp(data, "3DSX", 4) == 0);
|
||||||
|
if (!valid) { free(data); fail("Datei prüfen", -1); return false; }
|
||||||
|
bool ok = runningAsCia ? install_cia(data, size) : install_3dsx(data, size);
|
||||||
|
free(data);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schritt 2: neue Version laden und installieren (bei Fehlern bis zu 3 Versuche)
|
||||||
|
static void download_worker(void *arg)
|
||||||
|
{
|
||||||
|
char url[128];
|
||||||
|
mkdir("sdmc:/3ds/mbmnet", 0777);
|
||||||
|
remove(LOG_FILE);
|
||||||
|
errStep[0] = 0;
|
||||||
|
snprintf(url, sizeof(url), "http://%s/app/%s", server, runningAsCia ? "mbmnet.cia" : "mbmnet.3dsx");
|
||||||
|
ulog("MBMNET-Update v%s -> v%s (%s)", APP_VERSION, newVersion, runningAsCia ? "CIA" : "3DSX");
|
||||||
|
ulog("Lade %s", url);
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
||||||
|
if (attempt > 1) ulog("--- Versuch %d von %d", attempt, ATTEMPTS);
|
||||||
|
downloadAttempt = attempt;
|
||||||
|
if (download_and_install(url)) {
|
||||||
|
ulog("Ergebnis: OK");
|
||||||
|
state = UPD_INSTALLED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attempt < ATTEMPTS) pause_seconds(3);
|
||||||
|
}
|
||||||
|
ulog("Ergebnis: FEHLER nach %d Versuchen", ATTEMPTS);
|
||||||
|
state = UPD_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void start_thread(ThreadFunc fn)
|
||||||
|
{
|
||||||
|
if (thread) { threadJoin(thread, U64_MAX); threadFree(thread); thread = NULL; }
|
||||||
|
s32 prio = 0x30;
|
||||||
|
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||||
|
thread = threadCreate(fn, NULL, 32 * 1024, prio + 1, -2, false);
|
||||||
|
if (!thread) state = UPD_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_start(const char *srv, const char *self)
|
||||||
|
{
|
||||||
|
if (!srv[0] || state == UPD_CHECKING || state == UPD_DOWNLOADING) return;
|
||||||
|
snprintf(server, sizeof(server), "%s", srv);
|
||||||
|
snprintf(selfPath, sizeof(selfPath), "%s", self);
|
||||||
|
runningAsCia = !envIsHomebrew();
|
||||||
|
state = UPD_CHECKING;
|
||||||
|
start_thread(check_worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_download(void)
|
||||||
|
{
|
||||||
|
if (state != UPD_AVAILABLE) return;
|
||||||
|
state = UPD_DOWNLOADING;
|
||||||
|
start_thread(download_worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool update_pending(void)
|
||||||
|
{
|
||||||
|
return state == UPD_AVAILABLE || state == UPD_DOWNLOADING || state == UPD_INSTALLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_finish(void)
|
||||||
|
{
|
||||||
|
if (!thread) return;
|
||||||
|
// Läuft der Download noch, nicht freigeben: Die App beendet sich ohnehin.
|
||||||
|
if (R_SUCCEEDED(threadJoin(thread, 3ULL * 1000 * 1000 * 1000))) threadFree(thread);
|
||||||
|
thread = NULL;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <3ds/types.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
UPD_IDLE, // noch nicht geprüft
|
||||||
|
UPD_CHECKING, // fragt den Server
|
||||||
|
UPD_LATEST, // schon die neueste Version
|
||||||
|
UPD_AVAILABLE, // neue Version gefunden, wartet auf update_download()
|
||||||
|
UPD_DOWNLOADING, // lädt neue Version
|
||||||
|
UPD_INSTALLED, // neue Version liegt auf der SD, App neu starten
|
||||||
|
UPD_NONE, // Server bietet keine Updates an
|
||||||
|
UPD_ERROR, // Server nicht erreichbar o. Ä.
|
||||||
|
} UpdateState;
|
||||||
|
|
||||||
|
// Startet die Prüfung im Hintergrund. selfPath = Pfad der laufenden .3dsx.
|
||||||
|
void update_start(const char *server, const char *selfPath);
|
||||||
|
// Lädt und installiert die gefundene neue Version (nur im Zustand UPD_AVAILABLE).
|
||||||
|
void update_download(void);
|
||||||
|
// true, solange ein Update ansteht, lädt oder auf den Neustart wartet
|
||||||
|
bool update_pending(void);
|
||||||
|
UpdateState update_state(void);
|
||||||
|
const char *update_new_version(void);
|
||||||
|
void update_finish(void);
|
||||||
|
// Laufender Download-Versuch (1-3)
|
||||||
|
int update_attempt(void);
|
||||||
|
// Beschreibung des letzten Fehlers (Schritt und Code)
|
||||||
|
void update_error(char *out, size_t len);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Build-Umgebung für die MBMNET-App: devkitARM + makerom + bannertool (für die CIA)
|
||||||
|
FROM devkitpro/devkitarm
|
||||||
|
|
||||||
|
# makerom (Project_CTR) aus dem Quellcode bauen
|
||||||
|
RUN git clone --depth 1 https://github.com/3DSGuy/Project_CTR.git /tmp/ctr \
|
||||||
|
&& make -C /tmp/ctr/makerom deps \
|
||||||
|
&& make -C /tmp/ctr/makerom \
|
||||||
|
&& find /tmp/ctr/makerom -type f -name makerom -perm -u+x -exec cp {} /usr/local/bin/makerom \; \
|
||||||
|
&& rm -rf /tmp/ctr
|
||||||
|
|
||||||
|
# bannertool aus dem Quellcode bauen (direkt kompiliert, die mitgelieferten
|
||||||
|
# Build-Skripte kennen ARM-Rechner wie Apple Silicon nicht)
|
||||||
|
RUN git clone --depth 1 https://github.com/diasurgical/bannertool.git /tmp/bt \
|
||||||
|
&& cd /tmp/bt && mkdir o \
|
||||||
|
&& for f in $(find source -name '*.c'); do gcc -O2 -Isource -c "$f" -o "o/$(basename $f).o"; done \
|
||||||
|
&& for f in $(find source -name '*.cpp'); do g++ -std=gnu++11 -O2 -Isource -DVERSION_MAJOR=1 -DVERSION_MINOR=2 -DVERSION_MICRO=0 -c "$f" -o "o/$(basename $f).o"; done \
|
||||||
|
&& g++ o/*.o -o /usr/local/bin/bannertool \
|
||||||
|
&& rm -rf /tmp/bt
|
||||||
|
|
||||||
|
RUN makerom -version || true; bannertool 2>&1 | head -1 || true
|
||||||
Reference in New Issue
Block a user