MBMNET-App v2.8.2: Quellcode (GPLv3)

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

After

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+272
View File
@@ -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
+27
View File
@@ -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
+162
View File
@@ -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
+335
View File
@@ -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);
}
}
+10
View File
@@ -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);
+28
View File
@@ -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;
+181
View File
@@ -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(&notificationSemaphore); // 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;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Executable
+66
View File
@@ -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)."
+57
View File
@@ -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;
}
+9
View File
@@ -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);
+58
View File
@@ -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;
}
+6
View File
@@ -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);
+245
View File
@@ -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;
}
+27
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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 ..."; }
+16
View File
@@ -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);
+135
View File
@@ -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);
}
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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(&current, 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;
}
+23
View File
@@ -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);
+109
View File
@@ -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;
}
+11
View File
@@ -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);
+885
View File
@@ -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;
}
+30
View File
@@ -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);
+48
View File
@@ -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;
}
+6
View File
@@ -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);
+1985
View File
File diff suppressed because it is too large Load Diff
+249
View File
@@ -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;
}
+17
View File
@@ -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
View File
@@ -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;
}
+28
View File
@@ -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);
+20
View File
@@ -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