finish refactor, add docs and CI
CI / Build NRO (push) Failing after 7s
CI / Format check (push) Successful in 38s
CI / Layering check (push) Successful in 3s
CI / Upload release asset (push) Has been skipped

This commit is contained in:
2026-04-27 01:49:41 +03:00
parent dc65a4c8a9
commit a76e06ba4d
47 changed files with 1972 additions and 1470 deletions
+86
View File
@@ -0,0 +1,86 @@
name: CI
on:
push:
pull_request:
jobs:
nro:
name: Build NRO
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
docker run --rm \
-v "$PWD:/workspace" -w /workspace \
devkitpro/devkita64:latest \
bash -c "cmake --preset switch && cmake --build build -j\$(nproc)"
- uses: actions/upload-artifact@v4
with:
name: NXST-${{ gitea.sha }}
path: build/NXST.nro
release:
name: Upload release asset
runs-on: ubuntu-latest
needs: nro
if: startsWith(gitea.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v4
with:
name: NXST-${{ gitea.sha }}
- name: Create Gitea release and upload NRO
env:
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAG: ${{ gitea.ref_name }}
API: ${{ gitea.server_url }}/api/v1
REPO: ${{ gitea.repository }}
run: |
release_id=$(curl -sf -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\"}" \
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
curl -sf -X POST "$API/repos/$REPO/releases/$release_id/assets?name=NXST.nro" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @NXST.nro
format:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang-format
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update && sudo apt-get install -y clang-format-22
sudo ln -sf /usr/bin/clang-format-22 /usr/local/bin/clang-format
- name: Check formatting
run: |
find src include \( -name '*.cpp' -o -name '*.hpp' \) \
| xargs clang-format --dry-run --Werror
layering:
name: Layering check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: UI must not include net/sys headers
run: |
! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/
+76
View File
@@ -0,0 +1,76 @@
name: CI
on:
push:
pull_request:
jobs:
nro:
name: Build NRO
runs-on: ubuntu-latest
container:
image: devkitpro/devkita64:latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Configure
run: cmake --preset switch
- name: Build
run: cmake --build build -j$(nproc)
- uses: actions/upload-artifact@v4
with:
name: NXST-${{ github.sha }}
path: build/NXST.nro
release:
name: Upload release asset
runs-on: ubuntu-latest
needs: nro
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: NXST-${{ github.sha }}
- uses: softprops/action-gh-release@v2
with:
files: NXST.nro
format:
name: Format check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang-format
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm.list
sudo apt-get update && sudo apt-get install -y clang-format-22
sudo ln -sf /usr/bin/clang-format-22 /usr/local/bin/clang-format
- name: Check formatting
run: |
find src include \( -name '*.cpp' -o -name '*.hpp' \) \
| xargs clang-format --dry-run --Werror
layering:
name: Layering check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: UI must not include net/sys headers
run: |
! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/
+34
View File
@@ -0,0 +1,34 @@
# Changelog
All notable changes to NXST follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Version numbers are calendar-based (`MM.DD.YYYY`) until the protocol stabilises.
---
## [Unreleased]
### Changed
- Full architectural refactor (8 phases):
- Layered directory structure: `src/` + `include/nxst/` mirroring domain / infra / service / ui
- CMake build system replacing Makefile
- `nxst::TransferService` — all network state and threads moved out of UI layer
- `nxst::Result<T>` tagged-union type replacing `std::tuple<bool, Result, std::string>`
- RAII handles: `FsFileSystemHandle`, `FileHandle`
- Logger rewritten with `nxst::log` namespace and compile-time format checking
- snake_case filenames and identifiers throughout
### Fixed
- `mkdir(path, 777)` was decimal (octal `01411`) — corrected to `0777`
- Logger format args were silently dropped (double-substitution bug)
- `new u8[]` / `malloc` in IO paths replaced with `std::vector`
---
## [04.26.2026]
### Added
- Initial public release: save transfer between two Nintendo Switch consoles over local Wi-Fi
- Multicast discovery (UDP `239.0.0.1:8081`) + TCP file transfer (`:8080`)
- Send and Receive modes with progress overlay and cancel support
- Automatic save backup before transfer
- Restore replaces live save data and commits to the save device
+17
View File
@@ -0,0 +1,17 @@
{
"version": 6,
"configurePresets": [
{
"name": "switch",
"displayName": "Nintendo Switch",
"toolchainFile": "$env{DEVKITPRO}/cmake/Switch.cmake",
"binaryDir": "${sourceDir}/build"
}
],
"buildPresets": [
{
"name": "switch",
"configurePreset": "switch"
}
]
}
+30
View File
@@ -0,0 +1,30 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
For the full license text see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-------------------------------------------------------------------------------
NXST includes code derived from Checkpoint by Bernardo Giordano / FlagBrew,
licensed under GPLv3. The derived files are:
src/infra/fs/io.cpp
src/domain/account.cpp
src/domain/title.cpp
include/nxst/infra/fs/filesystem.hpp
src/infra/fs/filesystem.cpp
src/infra/fs/directory.cpp
All original NXST code is also released under the GNU General Public
License v3 or later, to satisfy the license inheritance requirement.
Original Checkpoint repository: https://github.com/BernardoGiordano/Checkpoint
-204
View File
@@ -1,204 +0,0 @@
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPRO)),)
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro")
endif
TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/libnx/switch_rules
#---------------------------------------------------------------------------------
# TARGET is the name of the output
# BUILD is the directory where object files & intermediate files will be placed
# SOURCES is a list of directories containing source code
# DATA is a list of directories containing data files
# INCLUDES is a list of directories containing header files
# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm".
# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional)
#
# NO_ICON: if set to anything, do not use icon.
# NO_NACP: if set to anything, no .nacp file is generated.
# APP_TITLE is the name of the app stored in the .nacp file (Optional)
# APP_AUTHOR is the author of the app stored in the .nacp file (Optional)
# APP_VERSION is the version of the app stored in the .nacp file (Optional)
# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional)
# ICON is the filename of the icon (.jpg), relative to the project folder.
# If not set, it attempts to use one of the following (in this order):
# - <Project name>.jpg
# - icon.jpg
# - <libnx folder>/default_icon.jpg
#---------------------------------------------------------------------------------
TARGET := NXST
BUILD := build
SOURCES := src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source
INCLUDES := include lib/Plutonium/include
EXEFS_SRC := exefs_src
APP_TITLE := NXST
APP_AUTHOR := DragonSpirit
APP_VERSION := 04.26.2026
ICON := icon.png
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CFLAGS += -g -O2 -ffunction-sections $(ARCH) $(DEFINES)
CFLAGS += $(INCLUDE) -D__SWITCH__ -D_GNU_SOURCE=1
CXXFLAGS:= $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 -g
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
PKGCONF := $(DEVKITPRO)/portlibs/switch/bin/aarch64-none-elf-pkg-config
PKG_LIBS := SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib
LIBS := -lpu $(shell $(PKGCONF) --libs $(PKG_LIBS)) -ldrm_nouveau -lharfbuzz -lfreetype -lz
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
# include and lib
#---------------------------------------------------------------------------------
LIBDIRS := $(PORTLIBS) $(LIBNX) $(CURDIR)/lib/Plutonium
#---------------------------------------------------------------------------------
# no real need to edit anything past this point unless you need to add additional
# rules for different file extensions
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
#---------------------------------------------------------------------------------
export LD := $(CC)
#---------------------------------------------------------------------------------
else
#---------------------------------------------------------------------------------
export LD := $(CXX)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES)))
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 BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC)
ifeq ($(strip $(ICON)),)
icons := $(wildcard *.jpg)
ifneq (,$(findstring $(TARGET).jpg,$(icons)))
export APP_ICON := $(TOPDIR)/$(TARGET).jpg
else
ifneq (,$(findstring icon.jpg,$(icons)))
export APP_ICON := $(TOPDIR)/icon.jpg
endif
endif
else
export APP_ICON := $(TOPDIR)/$(ICON)
endif
ifeq ($(strip $(NO_ICON)),)
export NROFLAGS += --icon=$(APP_ICON)
endif
ifeq ($(strip $(NO_NACP)),)
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
endif
ifneq ($(APP_TITLEID),)
export NACPFLAGS += --titleid=$(APP_TITLEID)
endif
ifneq ($(ROMFS),)
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
endif
.PHONY: $(BUILD) clean all
#---------------------------------------------------------------------------------
all: $(BUILD)
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).pfs0 $(TARGET).nso $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst
cleanbuild: clean all
#---------------------------------------------------------------------------------
send: $(BUILD)
@nxlink $(TARGET).nro
debug: $(BUILD)
@nxlink -s $(TARGET).nro
else
.PHONY: all
DEPENDS := $(OFILES:.o=.d)
#---------------------------------------------------------------------------------
# main targets
#---------------------------------------------------------------------------------
all : $(OUTPUT).pfs0 $(OUTPUT).nro
$(OUTPUT).pfs0 : $(OUTPUT).nso
$(OUTPUT).nso : $(OUTPUT).elf
ifeq ($(strip $(NO_NACP)),)
$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp
else
$(OUTPUT).nro : $(OUTPUT).elf
endif
$(OUTPUT).elf : $(OFILES)
$(OFILES_SRC) : $(HFILES_BIN)
#---------------------------------------------------------------------------------
# you need a rule like this for each extension you use as binary data
#---------------------------------------------------------------------------------
%.bin.o %_bin.h : %.bin
#---------------------------------------------------------------------------------
@echo $(notdir $<)
@$(bin2o)
-include $(DEPENDS)
#---------------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------------
+3 -3
View File
@@ -11,10 +11,10 @@
| 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain | | 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain |
| 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling | | 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling |
| 6 | `Result<T>` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory | | 6 | `Result<T>` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory |
| 7 | Documentation + license | ☐ Not started | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE | | 7 | Documentation + license | ✅ Done | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE |
| 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | | 8 | CI | ✅ Done | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check |
**Active phase:** Phase 7 — Documentation + license. **Active phase:** — All phases complete.
**Last updated:** 2026-04-27. **Last updated:** 2026-04-27.
Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth. Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth.
+88
View File
@@ -0,0 +1,88 @@
# NXST
Transfer save data between two Nintendo Switch consoles over local Wi-Fi.
Pick a game, pick a user. One Switch sends; the other receives. The sender backs up its own save first, so you can never lose data in transit.
---
## Install
1. Download `NXST.nro` from [Releases](../../releases).
2. Copy it to `/switch/NXST/NXST.nro` on your SD card.
3. Launch via hbmenu (hold R while starting any game, or Album).
Both Switches must be on the same Wi-Fi network. No router configuration needed — discovery uses UDP multicast.
---
## Usage
**Sender** (the Switch whose save you want to copy):
1. Open NXST → select a title → press **A****Transfer**.
2. Wait for "Waiting for receiver…" to change to "Transferring…".
**Receiver** (the Switch that will receive the save):
1. Open NXST → select the same title → press **A****Receive**.
2. Wait. The save is restored automatically when the transfer finishes.
Press **B** on either side to cancel mid-transfer.
Logs are written to `/switch/NXST/log.log`.
---
## Build
**Prerequisites:** [devkitPro](https://devkitpro.org/wiki/Getting_Started) with `switch-dev` and `switch-portlibs` packages, plus `cmake ≥ 3.20`.
```bash
# Clone with submodules (Plutonium UI)
git clone --recurse-submodules https://github.com/your-username/NXST.git
cd NXST
# Configure (once)
cmake --preset switch
# Build
cmake --build build
# Send to Switch via nxlink (Switch must be on same network, nxlink running)
cmake --build build --target send
```
Output: `build/NXST.nro`
---
## Architecture
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the layer diagram, threading model, and key types.
See [`docs/PROTOCOL.md`](docs/PROTOCOL.md) for the wire protocol (UDP multicast discovery + TCP file stream).
```
ui/ — TitlesLayout, UsersLayout, TransferOverlay, HeaderBar
service/ — TransferService (all network threads and state)
infra/net/ — Socket RAII, sendAll/recvAll
infra/fs/ — io::backup, io::restore, directory iterator, RAII handles
infra/sys/ — nxst::log (printf-checked, timestamped)
domain/ — Title, Account, Result<T>, TransferState, protocol constants
```
---
## Credits
- **[Plutonium](https://github.com/XorTroll/Plutonium)** — Switch UI framework by XorTroll
- **[Checkpoint](https://github.com/BernardoGiordano/Checkpoint)** — save management library by Bernardo Giordano / FlagBrew; several files in `src/infra/fs/` and `src/domain/` are derived from Checkpoint
---
## License
GPLv3 — see [`LICENSE`](LICENSE).
NXST includes code derived from Checkpoint (GPLv3). All original NXST code is released under the same license to satisfy the GPL inheritance requirement.
+163
View File
@@ -0,0 +1,163 @@
# NXST Architecture
## Layer Diagram
```
┌──────────────────────────────┐
│ ui/ │
│ TitlesLayout, UsersLayout │
│ TransferOverlay, HeaderBar │
└──────────────┬───────────────┘
│ calls
┌──────────────▼───────────────┐
│ service/ │
│ TransferService │
└──────┬───────────────┬───────┘
│ │
┌────────────▼──┐ ┌───────▼────────┐
│ infra/net/ │ │ infra/fs/ │
│ TransferService│ │ io, directory │
│ (sender thread │ │ filesystem │
│ recv threads) │ │ handles │
└────────────┬──┘ └───────┬────────┘
│ │
┌────────────▼───────────────▼────────┐
│ domain/ │
│ Title, Account, TransferState │
│ Result<T>, protocol constants │
└─────────────────────────────────────┘
┌────────────────────▼────────────────┐
│ libnx, Plutonium, SDL2, devkitpro│
└─────────────────────────────────────┘
```
**Hard layering rules:**
- `domain/` depends on nothing in the project.
- `infra/` depends only on `domain/`.
- `service/` depends on `domain/` + `infra/`.
- `ui/` depends only on `service/` + `domain/` — must not include `<arpa/inet.h>`, `<sys/socket.h>`, `<pthread.h>`, or call `recv`/`send` directly.
- `app/` (MainApplication, main.cpp) wires everything together.
---
## Key Types
### `nxst::TransferService` (`include/nxst/service/transfer_service.hpp`)
Owns all network state for both transfer directions. One instance lives in `MainApplication`.
| Method | Description |
|--------|-------------|
| `startSend(index, uid)` | Backup save, discover receiver, stream files |
| `startReceive(index, uid, title_name)` | Listen for sender, receive files, restore save |
| `cancelSend()` / `cancelReceive()` | Interrupt in-flight transfer (socket shutdown) |
| `isSendDone()` / `isReceiveDone()` | Polled by UI render loop |
| `restoreSucceeded()` / `restoreError()` | Restore outcome after receive |
**Threading model:**
```
UI thread (Plutonium event loop)
└─ startSend()
└─ senderEntry [pthread, detached]
├─ findServer() — UDP multicast, 100 ms poll slices
├─ io::backup() — creates local save backup
└─ TCP send loop
└─ startReceive()
├─ broadcastEntry [pthread, detached] — UDP multicast listener
└─ acceptEntry [pthread, detached]
├─ TCP receive loop
└─ io::restore() — mounts save, clears, copies, commits
```
Cancellation: `cancelSend()` / `cancelReceive()` call `shutdown(SHUT_RDWR)` on all open sockets. Blocking `read`/`recvfrom`/`accept` return errors immediately. Atomic flags are checked at loop boundaries.
### `nxst::Result<T, E>` (`include/nxst/domain/result.hpp`)
85-line tagged-union result type. No exceptions, no `std::variant`. Used for `io::backup` and `io::restore` return values.
```cpp
auto result = io::backup(title_index, uid);
if (!result.isOk()) {
failSend("Backup failed: " + result.error());
return;
}
fs::path dir = result.value();
```
`Result<void, E>` specialisation available for operations that succeed without a value.
### `nxst::FsFileSystemHandle` / `nxst::FileHandle` (`include/nxst/infra/fs/handles.hpp`)
RAII wrappers ensuring `fsFsClose` / `fclose` are called on all exit paths.
---
## Threading Invariants
- All `std::atomic` members in `TransferService` use sequential-consistency (default). No explicit `memory_order` needed at this concurrency level.
- `TransferState::status` is a `std::string` protected by `std::mutex status_mutex`. All other `TransferState` fields are atomics.
- `pthread_create` + `pthread_detach` is used throughout (libnx's supported path). Threads are never joined — they signal completion via `state.done = true` and set their active flag to false.
- Cancel is safe to call from any thread.
---
## File Map
```
include/nxst/
├── app/
│ ├── main.hpp — global state (g_currentTime, g_sortMode, …)
│ └── main_application.hpp — MainApplication : pu::ui::Application
├── domain/
│ ├── account.hpp — AccountUid, Account::ids(), Account::username()
│ ├── common.hpp — StringUtils (UTF-8/16, elide, accents)
│ ├── protocol.hpp — wire constants (ports, sentinel, buffer size)
│ ├── result.hpp — Result<T, E> tagged union
│ ├── title.hpp — Title, getTitle(), getTitleCount()
│ ├── transfer_state.hpp — TransferState (atomics + mutex-guarded status)
│ └── util.hpp — StringUtils helpers, blinkLed
├── infra/
│ ├── fs/
│ │ ├── directory.hpp — Directory iterator (libnx FsDir)
│ │ ├── filesystem.hpp — FileSystem::mount/unmount wrappers
│ │ ├── handles.hpp — FsFileSystemHandle, FileHandle (RAII)
│ │ └── io.hpp — io::backup, io::restore, io::copyFile, …
│ ├── net/
│ │ └── socket.hpp — Socket RAII wrapper (int fd)
│ └── sys/
│ └── logger.hpp — nxst::log::info/warn/error (printf-checked)
├── service/
│ └── transfer_service.hpp — TransferService
└── ui/
├── card.hpp — Card UI component
├── const.h — layout/color/font constants
├── header_bar.hpp — HeaderBar (title + user avatar row)
├── hint_bar.hpp — HintBar (button legend)
├── theme.hpp — color, radius, spacing, type tokens
├── titles_layout.hpp — TitlesLayout
├── transfer_overlay.hpp — TransferOverlay (progress modal)
├── ui_context.hpp — UiContext (selected user state)
└── users_layout.hpp — UsersLayout
```
---
## Build
```bash
# Configure (once — toolchain preset reads $DEVKITPRO automatically)
cmake --preset switch
# Build
cmake --build build
# Send to Switch via nxlink
cmake --build build --target send
```
Output: `build/NXST.nro`
See `README.md` for full build prerequisites.
+95
View File
@@ -0,0 +1,95 @@
# NXST Wire Protocol
## Overview
NXST uses two sockets per transfer session:
| Socket | Purpose |
|--------|---------|
| UDP multicast | Receiver advertisement (discovery) |
| TCP | File data stream |
Both sides must be on the same local network segment. The sender (Transfer mode) initiates; the receiver (Receive mode) listens.
---
## Discovery — UDP Multicast
| Parameter | Value |
|-----------|-------|
| Group | `239.0.0.1` |
| Port | `8081` |
| Direction | sender → receiver |
**Flow:**
1. Receiver joins multicast group and binds `0.0.0.0:8081`.
2. Sender sends `"DISCOVER_SERVER"` (15 bytes, no null terminator) to `239.0.0.1:8081`.
3. Receiver replies `"SERVER_HERE"` (11 bytes) to the sender's source address.
4. Sender extracts the receiver's IP from the reply source and closes the UDP socket.
Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice.
---
## File Transfer — TCP
| Parameter | Value |
|-----------|-------|
| Port | `8080` |
| Direction | sender connects → receiver listens |
| Buffer size | 65 536 bytes (`proto::BUF_SIZE`) |
**Connection:**
1. Receiver listens on `0.0.0.0:8080` (started concurrently with multicast listener).
2. Sender connects after receiving `"SERVER_HERE"`.
**Wire layout — one file:**
```
┌─────────────────────────────────┐
│ filename_len │ uint32_t LE │ 4 bytes
├─────────────────────────────────┤
│ filename │ filename_len │ bytes, no null terminator
│ │ bytes │
├─────────────────────────────────┤
│ file_size │ uint64_t LE │ 8 bytes
├─────────────────────────────────┤
│ file_data │ file_size │ bytes
│ │ bytes │
└─────────────────────────────────┘
```
Files are sent sequentially. The stream ends with a sentinel frame:
```
filename_len == 0 (proto::EOF_SENTINEL)
```
No `filename` or `file_size` field follows the sentinel.
**Constraints:**
- `filename_len > proto::MAX_FILENAME` (4 096) is treated as a protocol error; the receiver aborts.
- Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST/<title>/<user>/...`).
- On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk.
---
## Post-Transfer
After the TCP stream closes (sentinel received), the receiver calls `io::restore`:
1. Mounts the title's save filesystem.
2. Clears existing save data.
3. Copies received files into `save:/`.
4. Commits via `fsdevCommitDevice("save")`.
The sender creates a local backup via `io::backup` before connecting, so the sender's own save is never at risk.
---
## Cancellation
Either side can cancel at any time by closing the relevant socket (`shutdown` + `close`). The other side's blocking read/write will return an error and the transfer loop exits cleanly. The receiver's accept thread sets `receiver_state.done = true` regardless of how the connection ends, so the UI poll loop always terminates.
+5 -4
View File
@@ -1,11 +1,13 @@
#pragma once #pragma once
#include <nxst/ui/const.h> #include <memory>
#include <switch.h>
#include <nxst/domain/account.hpp> #include <nxst/domain/account.hpp>
#include <nxst/domain/title.hpp> #include <nxst/domain/title.hpp>
#include <nxst/domain/util.hpp> #include <nxst/domain/util.hpp>
#include <memory>
#include <switch.h>
#include <nxst/infra/sys/logger.hpp> #include <nxst/infra/sys/logger.hpp>
#include <nxst/ui/const.h>
typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t; typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t;
@@ -20,4 +22,3 @@ inline sort_t g_sortMode = SORT_ALPHA;
inline std::string g_currentFile = ""; inline std::string g_currentFile = "";
inline bool g_isTransferringFile = false; inline bool g_isTransferringFile = false;
inline const std::string g_emptySave = "New..."; inline const std::string g_emptySave = "New...";
+3 -2
View File
@@ -1,9 +1,10 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <nxst/service/transfer_service.hpp> #include <nxst/service/transfer_service.hpp>
#include <nxst/ui/users_layout.hpp>
#include <nxst/ui/titles_layout.hpp> #include <nxst/ui/titles_layout.hpp>
#include <nxst/ui/users_layout.hpp>
namespace ui { namespace ui {
@@ -19,4 +20,4 @@ namespace ui {
TitlesLayout::Ref titles_layout; TitlesLayout::Ref titles_layout;
nxst::TransferService transfer; nxst::TransferService transfer;
}; };
} } // namespace ui
+13 -14
View File
@@ -28,31 +28,31 @@
#include <map> #include <map>
#include <string.h> #include <string.h>
#include <string> #include <string>
#include <switch.h>
#include <vector> #include <vector>
#include <switch.h>
#define USER_ICON_SIZE 64 #define USER_ICON_SIZE 64
namespace std { namespace std {
template <> template <> struct hash<AccountUid> {
struct hash<AccountUid> { size_t operator()(const AccountUid& a) const {
size_t operator()(const AccountUid& a) const { return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1); } return ((hash<u64>()(a.uid[0]) ^ (hash<u64>()(a.uid[1]) << 1)) >> 1);
};
} }
};
} // namespace std
inline bool operator==(const AccountUid& x, const AccountUid& y) inline bool operator==(const AccountUid& x, const AccountUid& y) {
{
return x.uid[0] == y.uid[0] && x.uid[1] == y.uid[1]; return x.uid[0] == y.uid[0] && x.uid[1] == y.uid[1];
} }
inline bool operator==(const AccountUid& x, u64 y) inline bool operator==(const AccountUid& x, u64 y) {
{
return x.uid[0] == y && x.uid[1] == y; return x.uid[0] == y && x.uid[1] == y;
} }
inline bool operator<(const AccountUid& x, const AccountUid& y) inline bool operator<(const AccountUid& x, const AccountUid& y) {
{ if (x.uid[0] != y.uid[0])
if (x.uid[0] != y.uid[0]) return x.uid[0] < y.uid[0]; return x.uid[0] < y.uid[0];
return x.uid[1] < y.uid[1]; return x.uid[1] < y.uid[1];
} }
@@ -69,5 +69,4 @@ namespace Account {
AccountUid selectAccount(void); AccountUid selectAccount(void);
std::string username(AccountUid id); std::string username(AccountUid id);
std::string iconPath(AccountUid id); std::string iconPath(AccountUid id);
} } // namespace Account
+2 -3
View File
@@ -45,7 +45,7 @@ namespace DateTime {
std::string timeStr(void); std::string timeStr(void);
std::string dateTimeStr(void); std::string dateTimeStr(void);
std::string logDateTime(void); std::string logDateTime(void);
} } // namespace DateTime
namespace StringUtils { namespace StringUtils {
bool containsInvalidChar(const std::string& str); bool containsInvalidChar(const std::string& str);
@@ -56,7 +56,6 @@ namespace StringUtils {
void ltrim(std::string& s); void ltrim(std::string& s);
void rtrim(std::string& s); void rtrim(std::string& s);
void trim(std::string& s); void trim(std::string& s);
} } // namespace StringUtils
char* getConsoleIP(void); char* getConsoleIP(void);
+1 -1
View File
@@ -14,4 +14,4 @@ namespace proto {
// [filename : filename_len bytes] // [filename : filename_len bytes]
// [file_size : uint64_t LE] // [file_size : uint64_t LE]
// [file_data : file_size bytes] // [file_data : file_size bytes]
} } // namespace proto
+42 -19
View File
@@ -4,8 +4,7 @@
namespace nxst { namespace nxst {
template <class T, class E = std::string> template <class T, class E = std::string> class Result {
class Result {
bool ok; bool ok;
alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)]; alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)];
@@ -27,37 +26,52 @@ public:
} }
~Result() { ~Result() {
if (ok) reinterpret_cast<T*>(storage)->~T(); if (ok)
else reinterpret_cast<E*>(storage)->~E(); reinterpret_cast<T*>(storage)->~T();
else
reinterpret_cast<E*>(storage)->~E();
} }
Result(const Result& other) : ok(other.ok) { Result(const Result& other) : ok(other.ok) {
if (ok) new (storage) T(*reinterpret_cast<const T*>(other.storage)); if (ok)
else new (storage) E(*reinterpret_cast<const E*>(other.storage)); new (storage) T(*reinterpret_cast<const T*>(other.storage));
else
new (storage) E(*reinterpret_cast<const E*>(other.storage));
} }
Result(Result&& other) : ok(other.ok) { Result(Result&& other) : ok(other.ok) {
if (ok) new (storage) T(std::move(*reinterpret_cast<T*>(other.storage))); if (ok)
else new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); new (storage) T(std::move(*reinterpret_cast<T*>(other.storage)));
else
new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
} }
Result& operator=(const Result&) = delete; Result& operator=(const Result&) = delete;
bool isOk() const noexcept { return ok; } bool isOk() const noexcept {
const T& value() const { return *reinterpret_cast<const T*>(storage); } return ok;
const E& error() const { return *reinterpret_cast<const E*>(storage); } }
const T& value() const {
return *reinterpret_cast<const T*>(storage);
}
const E& error() const {
return *reinterpret_cast<const E*>(storage);
}
}; };
// Specialisation for Result<void> // Specialisation for Result<void>
template <class E> template <class E> class Result<void, E> {
class Result<void, E> {
bool ok; bool ok;
alignas(E) unsigned char storage[sizeof(E)]; alignas(E) unsigned char storage[sizeof(E)];
Result() = default; Result() = default;
public: public:
static Result success() { Result res; res.ok = true; return res; } static Result success() {
Result res;
res.ok = true;
return res;
}
static Result failure(E err) { static Result failure(E err) {
Result res; Result res;
@@ -66,20 +80,29 @@ public:
return res; return res;
} }
~Result() { if (!ok) reinterpret_cast<E*>(storage)->~E(); } ~Result() {
if (!ok)
reinterpret_cast<E*>(storage)->~E();
}
Result(const Result& other) : ok(other.ok) { Result(const Result& other) : ok(other.ok) {
if (!ok) new (storage) E(*reinterpret_cast<const E*>(other.storage)); if (!ok)
new (storage) E(*reinterpret_cast<const E*>(other.storage));
} }
Result(Result&& other) : ok(other.ok) { Result(Result&& other) : ok(other.ok) {
if (!ok) new (storage) E(std::move(*reinterpret_cast<E*>(other.storage))); if (!ok)
new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
} }
Result& operator=(const Result&) = delete; Result& operator=(const Result&) = delete;
bool isOk() const noexcept { return ok; } bool isOk() const noexcept {
const E& error() const { return *reinterpret_cast<const E*>(storage); } return ok;
}
const E& error() const {
return *reinterpret_cast<const E*>(storage);
}
}; };
} // namespace nxst } // namespace nxst
+8 -6
View File
@@ -25,20 +25,23 @@
*/ */
#pragma once #pragma once
#include <nxst/domain/account.hpp>
#include <nxst/infra/fs/filesystem.hpp>
#include <nxst/infra/fs/io.hpp>
#include <algorithm> #include <algorithm>
#include <stdlib.h> #include <stdlib.h>
#include <string> #include <string>
#include <switch.h>
#include <unordered_map> #include <unordered_map>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <switch.h>
#include <nxst/domain/account.hpp>
#include <nxst/infra/fs/filesystem.hpp>
#include <nxst/infra/fs/io.hpp>
class Title { class Title {
public: public:
void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name, const std::string& author); void init(u8 saveDataType, u64 titleid, AccountUid userID, const std::string& name,
const std::string& author);
~Title() = default; ~Title() = default;
std::string author(void); std::string author(void);
@@ -85,4 +88,3 @@ void sortTitles(void);
void rotateSortMode(void); void rotateSortMode(void);
void refreshDirectories(u64 id); void refreshDirectories(u64 id);
std::unordered_map<std::string, std::string> getCompleteTitleList(void); std::unordered_map<std::string, std::string> getCompleteTitleList(void);
+5 -5
View File
@@ -25,11 +25,13 @@
*/ */
#pragma once #pragma once
#include <sys/stat.h>
#include <switch.h>
#include <nxst/domain/account.hpp> #include <nxst/domain/account.hpp>
#include <nxst/domain/common.hpp> #include <nxst/domain/common.hpp>
#include <nxst/infra/fs/io.hpp> #include <nxst/infra/fs/io.hpp>
#include <switch.h>
#include <sys/stat.h>
// debug // debug
#include <arpa/inet.h> #include <arpa/inet.h>
@@ -46,6 +48,4 @@ namespace StringUtils {
std::string removeNotAscii(std::string str); std::string removeNotAscii(std::string str);
std::u16string UTF8toUTF16(const char* src); std::u16string UTF8toUTF16(const char* src);
std::string elide(const std::string& s, size_t maxChars); std::string elide(const std::string& s, size_t maxChars);
} } // namespace StringUtils
+2 -2
View File
@@ -28,9 +28,10 @@
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
#include <string> #include <string>
#include <switch.h>
#include <vector> #include <vector>
#include <switch.h>
struct DirectoryEntry { struct DirectoryEntry {
std::string name; std::string name;
bool directory; bool directory;
@@ -52,4 +53,3 @@ private:
Result mError; Result mError;
bool mGood; bool mGood;
}; };
+3 -3
View File
@@ -25,12 +25,12 @@
*/ */
#pragma once #pragma once
#include <nxst/domain/account.hpp>
#include <switch.h> #include <switch.h>
#include <nxst/domain/account.hpp>
namespace FileSystem { namespace FileSystem {
Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID); Result mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID);
int mount(FsFileSystem fs); int mount(FsFileSystem fs);
void unmount(void); void unmount(void);
} } // namespace FileSystem
+21 -6
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <cstdio> #include <cstdio>
#include <switch.h> #include <switch.h>
namespace nxst { namespace nxst {
@@ -10,14 +11,21 @@ struct FsFileSystemHandle {
bool valid{false}; bool valid{false};
FsFileSystemHandle() = default; FsFileSystemHandle() = default;
~FsFileSystemHandle() { if (valid) fsFsClose(&fs); } // NOLINT(modernize-use-equals-default) ~FsFileSystemHandle() {
if (valid)
fsFsClose(&fs);
} // NOLINT(modernize-use-equals-default)
FsFileSystemHandle(const FsFileSystemHandle&) = delete; FsFileSystemHandle(const FsFileSystemHandle&) = delete;
FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete; FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete;
FsFileSystem* get() { return &fs; } FsFileSystem* get() {
return &fs;
}
void release() { valid = false; } // transfer ownership to devfs void release() {
valid = false;
} // transfer ownership to devfs
}; };
// RAII wrapper for FILE* — auto-fclose on destruction. // RAII wrapper for FILE* — auto-fclose on destruction.
@@ -25,13 +33,20 @@ struct FileHandle {
FILE* ptr{nullptr}; FILE* ptr{nullptr};
explicit FileHandle(FILE* file) : ptr(file) {} explicit FileHandle(FILE* file) : ptr(file) {}
~FileHandle() { if (ptr != nullptr) fclose(ptr); } // NOLINT(modernize-use-equals-default) ~FileHandle() {
if (ptr != nullptr)
fclose(ptr);
} // NOLINT(modernize-use-equals-default)
FileHandle(const FileHandle&) = delete; FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete; FileHandle& operator=(const FileHandle&) = delete;
explicit operator bool() const { return ptr != nullptr; } explicit operator bool() const {
FILE* get() const { return ptr; } return ptr != nullptr;
}
FILE* get() const {
return ptr;
}
}; };
} // namespace nxst } // namespace nxst
+11 -9
View File
@@ -25,21 +25,24 @@
*/ */
#pragma once #pragma once
#include <nxst/domain/account.hpp>
#include <nxst/domain/result.hpp>
#include <nxst/infra/fs/directory.hpp>
#include <nxst/domain/title.hpp>
#include <nxst/domain/util.hpp>
#include <dirent.h> #include <dirent.h>
#include <switch.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <switch.h>
#include <nxst/domain/account.hpp>
#include <nxst/domain/result.hpp>
#include <nxst/domain/title.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/directory.hpp>
#define BUFFER_SIZE 0x80000 #define BUFFER_SIZE 0x80000
namespace io { namespace io {
nxst::Result<std::string> backup(size_t index, AccountUid uid); nxst::Result<std::string> backup(size_t index, AccountUid uid);
nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell); nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex,
const std::string& nameFromCell);
Result copyDirectory(const std::string& srcPath, const std::string& dstPath); Result copyDirectory(const std::string& srcPath, const std::string& dstPath);
void copyFile(const std::string& srcPath, const std::string& dstPath); void copyFile(const std::string& srcPath, const std::string& dstPath);
@@ -47,5 +50,4 @@ namespace io {
Result deleteFolderRecursively(const std::string& path); Result deleteFolderRecursively(const std::string& path);
bool directoryExists(const std::string& path); bool directoryExists(const std::string& path);
bool fileExists(const std::string& path); bool fileExists(const std::string& path);
} } // namespace io
+16 -5
View File
@@ -6,13 +6,24 @@ struct Socket {
Socket() = default; Socket() = default;
explicit Socket(int fd) : fd(fd) {} explicit Socket(int fd) : fd(fd) {}
~Socket() { if (fd >= 0) close(fd); } ~Socket() {
if (fd >= 0)
close(fd);
}
Socket(const Socket&) = delete; Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete; Socket& operator=(const Socket&) = delete;
Socket(Socket&& o) : fd(o.fd) { o.fd = -1; } Socket(Socket&& o) : fd(o.fd) {
o.fd = -1;
}
operator int() const { return fd; } operator int() const {
bool valid() const { return fd >= 0; } return fd;
void release() { fd = -1; } }
bool valid() const {
return fd >= 0;
}
void release() {
fd = -1;
}
}; };
+13 -9
View File
@@ -23,8 +23,7 @@ inline void flush() {}
// unchanged. Format args are dropped (same behavior as broken original). Migrate // unchanged. Format args are dropped (same behavior as broken original). Migrate
// call sites to nxst::log::* in Phase 3. // call sites to nxst::log::* in Phase 3.
struct Logger { struct Logger {
static Logger& getInstance() static Logger& getInstance() {
{
static Logger instance; static Logger instance;
return instance; return instance;
} }
@@ -35,16 +34,21 @@ struct Logger {
static constexpr const char* ERROR = "[ERROR]"; // NOLINT(readability-identifier-naming) static constexpr const char* ERROR = "[ERROR]"; // NOLINT(readability-identifier-naming)
static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming) static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming)
static void flush() { nxst::log::flush(); } static void flush() {
nxst::log::flush();
}
// Args intentionally dropped — format string still logged for visibility. // Args intentionally dropped — format string still logged for visibility.
template <typename... Args> template <typename... Args>
void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) void log(const std::string& level, const std::string& fmt, Args&&... /*args*/) {
{ if (level == ERROR)
if (level == ERROR) nxst::log::error("%s", fmt.c_str()); nxst::log::error("%s", fmt.c_str());
else if (level == WARN) nxst::log::warn("%s", fmt.c_str()); else if (level == WARN)
else if (level == DEBUG) nxst::log::debug("%s", fmt.c_str()); nxst::log::warn("%s", fmt.c_str());
else nxst::log::info("%s", fmt.c_str()); else if (level == DEBUG)
nxst::log::debug("%s", fmt.c_str());
else
nxst::log::info("%s", fmt.c_str());
} }
Logger() = default; Logger() = default;
+53 -16
View File
@@ -2,7 +2,9 @@
#include <atomic> #include <atomic>
#include <pthread.h> #include <pthread.h>
#include <string> #include <string>
#include <switch.h> #include <switch.h>
#include <nxst/domain/transfer_state.hpp> #include <nxst/domain/transfer_state.hpp>
namespace nxst { namespace nxst {
@@ -12,27 +14,55 @@ public:
int startSend(size_t title_index, AccountUid uid); int startSend(size_t title_index, AccountUid uid);
void cancelSend(); void cancelSend();
bool isSendDone() const { return sender_state.done.load(); } bool isSendDone() const {
bool isSendCancelled() const { return sender_state.cancelled.load(); } return sender_state.done.load();
bool isSendConnectionFailed() const { return sender_state.connection_failed.load(); } }
bool isSendProgressKnown() const { return sender_state.bytes_total.load() > 0; } bool isSendCancelled() const {
bool isSendWorkersIdle() const { return !sender_active.load(); } return sender_state.cancelled.load();
double sendProgress() const { return sender_state.progress(); } }
std::string sendStatusText() const { return sender_state.getStatus(); } bool isSendConnectionFailed() const {
std::string sendFailReason() const { return sender_state.fail_reason; } return sender_state.connection_failed.load();
}
bool isSendProgressKnown() const {
return sender_state.bytes_total.load() > 0;
}
bool isSendWorkersIdle() const {
return !sender_active.load();
}
double sendProgress() const {
return sender_state.progress();
}
std::string sendStatusText() const {
return sender_state.getStatus();
}
std::string sendFailReason() const {
return sender_state.fail_reason;
}
int startReceive(size_t title_index, AccountUid uid, std::string title_name); int startReceive(size_t title_index, AccountUid uid, std::string title_name);
void cancelReceive(); void cancelReceive();
bool isReceiveDone() const { return receiver_state.done.load(); } bool isReceiveDone() const {
bool isReceiveCancelled() const { return receiver_state.cancelled.load(); } return receiver_state.done.load();
}
bool isReceiveCancelled() const {
return receiver_state.cancelled.load();
}
bool isReceiveWorkersIdle() const { bool isReceiveWorkersIdle() const {
return !receiver_accept_active.load() && !receiver_broadcast_active.load(); return !receiver_accept_active.load() && !receiver_broadcast_active.load();
} }
double receiveProgress() const { return receiver_state.progress(); } double receiveProgress() const {
std::string receiveStatusText() const { return receiver_state.getStatus(); } return receiver_state.progress();
bool restoreSucceeded() const { return restore_ok; } }
std::string restoreError() const { return restore_error; } std::string receiveStatusText() const {
return receiver_state.getStatus();
}
bool restoreSucceeded() const {
return restore_ok;
}
std::string restoreError() const {
return restore_error;
}
private: private:
// Sender // Sender
@@ -58,14 +88,21 @@ private:
std::string restore_error; std::string restore_error;
// Sender thread // Sender thread
struct SenderArgs { TransferService* svc; size_t title_index; AccountUid uid; }; struct SenderArgs {
TransferService* svc;
size_t title_index;
AccountUid uid;
};
static void* senderEntry(void* arg); static void* senderEntry(void* arg);
void runSender(size_t title_index, AccountUid uid); void runSender(size_t title_index, AccountUid uid);
void failSend(const std::string& reason); void failSend(const std::string& reason);
int findServer(char* out_ip); int findServer(char* out_ip);
// Receiver threads // Receiver threads
struct AcceptArgs { TransferService* svc; int server_fd; }; struct AcceptArgs {
TransferService* svc;
int server_fd;
};
static void* broadcastEntry(void* arg); static void* broadcastEntry(void* arg);
static void* acceptEntry(void* arg); static void* acceptEntry(void* arg);
void runBroadcast(); void runBroadcast();
+3 -3
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <nxst/ui/theme.hpp> #include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
@@ -8,11 +9,10 @@ namespace ui {
public: public:
pu::ui::elm::Rectangle::Ref bg; pu::ui::elm::Rectangle::Ref bg;
Card(pu::ui::Layout* parent, int x, int y, int w, int h, Card(pu::ui::Layout* parent, int x, int y, int w, int h, pu::ui::Color color = theme::color::BgSurface,
pu::ui::Color color = theme::color::BgSurface,
int rad = theme::radius::lg) { int rad = theme::radius::lg) {
bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad); bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad);
parent->Add(bg); parent->Add(bg);
} }
}; };
} } // namespace ui
+6 -9
View File
@@ -1,8 +1,9 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <nxst/domain/account.hpp>
#include <nxst/ui/theme.hpp> #include <nxst/ui/theme.hpp>
#include <nxst/ui/ui_context.hpp> #include <nxst/ui/ui_context.hpp>
#include <nxst/domain/account.hpp>
namespace ui { namespace ui {
@@ -20,10 +21,8 @@ namespace ui {
HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") { HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") {
using namespace theme; using namespace theme;
bg = pu::ui::elm::Rectangle::New( bg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface);
0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface); divider = pu::ui::elm::Rectangle::New(0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider);
divider = pu::ui::elm::Rectangle::New(
0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider);
appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST"); appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST");
appName->SetFont(type::font(type::Title)); appName->SetFont(type::font(type::Title));
@@ -35,9 +34,7 @@ namespace ui {
const int chipW = 280; const int chipW = 280;
const int chipX = layout::ScreenW - chipW - space::lg; const int chipX = layout::ScreenW - chipW - space::lg;
chipBg = pu::ui::elm::Rectangle::New( chipBg = pu::ui::elm::Rectangle::New(chipX, 16, chipW, 40, color::BgSurface2, radius::pill);
chipX, 16, chipW, 40,
color::BgSurface2, radius::pill);
chipBg->SetVisible(false); chipBg->SetVisible(false);
avatar = pu::ui::elm::Image::New(chipX + 4, 20, ""); avatar = pu::ui::elm::Image::New(chipX + 4, 20, "");
@@ -83,4 +80,4 @@ namespace ui {
subtitle->SetText(text); subtitle->SetText(text);
} }
}; };
} } // namespace ui
+12 -11
View File
@@ -1,8 +1,10 @@
#pragma once #pragma once
#include <pu/Plutonium>
#include <nxst/ui/theme.hpp>
#include <vector>
#include <string> #include <string>
#include <vector>
#include <pu/Plutonium>
#include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
@@ -21,19 +23,18 @@ namespace ui {
public: public:
HintBar(pu::ui::Layout* p) : parent(p) { HintBar(pu::ui::Layout* p) : parent(p) {
using namespace theme; using namespace theme;
bg = pu::ui::elm::Rectangle::New( bg = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, layout::HintH,
0, layout::ScreenH - layout::HintH, color::BgSurface);
layout::ScreenW, layout::HintH, color::BgSurface); divider = pu::ui::elm::Rectangle::New(0, layout::ScreenH - layout::HintH, layout::ScreenW, 1,
divider = pu::ui::elm::Rectangle::New( color::Divider);
0, layout::ScreenH - layout::HintH,
layout::ScreenW, 1, color::Divider);
parent->Add(bg); parent->Add(bg);
parent->Add(divider); parent->Add(divider);
} }
void SetHints(const std::vector<Hint>& hints) { void SetHints(const std::vector<Hint>& hints) {
using namespace theme; using namespace theme;
for (auto& l : labels) l->SetVisible(false); for (auto& l : labels)
l->SetVisible(false);
labels.clear(); labels.clear();
int x = layout::ScreenW - space::lg; int x = layout::ScreenW - space::lg;
@@ -52,4 +53,4 @@ namespace ui {
} }
} }
}; };
} } // namespace ui
+10 -9
View File
@@ -1,8 +1,9 @@
#pragma once #pragma once
#include <pu/Plutonium>
#include <string> #include <string>
#include <pu/Plutonium>
namespace theme { namespace theme {
using pu::ui::Color; using pu::ui::Color;
@@ -26,7 +27,7 @@ namespace theme {
constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF}; constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF};
constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF}; constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF};
} } // namespace color
namespace space { namespace space {
constexpr int xs = 4; constexpr int xs = 4;
@@ -35,14 +36,14 @@ namespace theme {
constexpr int lg = 24; constexpr int lg = 24;
constexpr int xl = 32; constexpr int xl = 32;
constexpr int xxl = 48; constexpr int xxl = 48;
} } // namespace space
namespace radius { namespace radius {
constexpr int sm = 6; constexpr int sm = 6;
constexpr int md = 12; constexpr int md = 12;
constexpr int lg = 20; constexpr int lg = 20;
constexpr int pill = 9999; constexpr int pill = 9999;
} } // namespace radius
namespace type { namespace type {
constexpr int Display = 38; constexpr int Display = 38;
@@ -54,7 +55,7 @@ namespace theme {
inline std::string font(int size) { inline std::string font(int size) {
return "DefaultFont@" + std::to_string(size); return "DefaultFont@" + std::to_string(size);
} }
} } // namespace type
namespace layout { namespace layout {
constexpr int ScreenW = 1280; constexpr int ScreenW = 1280;
@@ -63,16 +64,16 @@ namespace theme {
constexpr int HintH = 56; constexpr int HintH = 56;
constexpr int ContentTop = HeaderH; constexpr int ContentTop = HeaderH;
constexpr int ContentH = ScreenH - HeaderH - HintH; constexpr int ContentH = ScreenH - HeaderH - HintH;
} } // namespace layout
namespace motion { namespace motion {
constexpr int FadeFrames = 20; constexpr int FadeFrames = 20;
constexpr int SlideFrames = 14; constexpr int SlideFrames = 14;
constexpr int SpinnerFrames = 72; constexpr int SpinnerFrames = 72;
} } // namespace motion
namespace font { namespace font {
constexpr const char* Default = "Inter"; constexpr const char* Default = "Inter";
constexpr const char* Medium = "InterMedium"; constexpr const char* Medium = "InterMedium";
} } // namespace font
} } // namespace theme
+14 -10
View File
@@ -1,11 +1,13 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <memory>
#include <nxst/ui/const.h>
#include <nxst/domain/title.hpp>
#include <nxst/domain/account.hpp>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <memory>
#include <pu/Plutonium>
#include <nxst/domain/account.hpp>
#include <nxst/domain/title.hpp>
#include <nxst/ui/const.h>
#include <nxst/ui/header_bar.hpp> #include <nxst/ui/header_bar.hpp>
#include <nxst/ui/hint_bar.hpp> #include <nxst/ui/hint_bar.hpp>
@@ -16,7 +18,6 @@ namespace ui {
class TitlesLayout : public pu::ui::Layout { class TitlesLayout : public pu::ui::Layout {
private: private:
pu::ui::elm::Menu::Ref titlesMenu; pu::ui::elm::Menu::Ref titlesMenu;
std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache; std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache;
bool m_inputLocked = false; bool m_inputLocked = false;
@@ -46,14 +47,17 @@ namespace ui {
void runReceive(int index, Title& title); void runReceive(int index, Title& title);
public: public:
TitlesLayout(); TitlesLayout();
void InitTitles(AccountUid uid); void InitTitles(AccountUid uid);
void LockInput() { m_inputLocked = true; } void LockInput() {
void UnlockInput() { m_inputLocked = false; } m_inputLocked = true;
}
void UnlockInput() {
m_inputLocked = false;
}
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
PU_SMART_CTOR(TitlesLayout) PU_SMART_CTOR(TitlesLayout)
}; };
} } // namespace ui
+12 -22
View File
@@ -1,7 +1,8 @@
#pragma once #pragma once
#include <pu/Plutonium> #include <pu/Plutonium>
#include <nxst/ui/theme.hpp>
#include <nxst/domain/util.hpp> #include <nxst/domain/util.hpp>
#include <nxst/ui/theme.hpp>
namespace ui { namespace ui {
@@ -22,22 +23,16 @@ namespace ui {
public: public:
TransferOverlay(const std::string& title) TransferOverlay(const std::string& title)
: Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) : Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim) {
{
using namespace theme; using namespace theme;
card = pu::ui::elm::Rectangle::New( card = pu::ui::elm::Rectangle::New(CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
titleText = pu::ui::elm::TextBlock::New( titleText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg, title);
CardX + space::lg, CardY + space::lg, title);
titleText->SetFont(type::font(type::Title)); titleText->SetFont(type::font(type::Title));
titleText->SetColor(color::TextPrimary); titleText->SetColor(color::TextPrimary);
statusText = pu::ui::elm::TextBlock::New( statusText = pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + space::lg + 56, "");
CardX + space::lg,
CardY + space::lg + 56,
"");
statusText->SetFont(type::font(type::Body)); statusText->SetFont(type::font(type::Body));
statusText->SetColor(color::TextSecondary); statusText->SetColor(color::TextSecondary);
@@ -45,24 +40,19 @@ namespace ui {
int barY = CardY + space::lg + 56 + 56; int barY = CardY + space::lg + 56 + 56;
int barW = CardW - 2 * space::lg; int barW = CardW - 2 * space::lg;
progressTrack = pu::ui::elm::Rectangle::New( progressTrack = pu::ui::elm::Rectangle::New(barX, barY, barW, 8, color::Divider, radius::sm);
barX, barY, barW, 8, color::Divider, radius::sm);
progressBar = pu::ui::elm::ProgressBar::New( progressBar = pu::ui::elm::ProgressBar::New(barX, barY, barW, 8, 100.0);
barX, barY, barW, 8, 100.0);
progressBar->SetProgressColor(color::Primary); progressBar->SetProgressColor(color::Primary);
progressBar->SetBackgroundColor(color::Divider); progressBar->SetBackgroundColor(color::Divider);
indeterminateText = pu::ui::elm::TextBlock::New( indeterminateText = pu::ui::elm::TextBlock::New(barX, barY - 4, "Preparing transfer...");
barX, barY - 4, "Preparing transfer...");
indeterminateText->SetFont(type::font(type::Body)); indeterminateText->SetFont(type::font(type::Body));
indeterminateText->SetColor(color::TextMuted); indeterminateText->SetColor(color::TextMuted);
indeterminateText->SetVisible(false); indeterminateText->SetVisible(false);
hintText = pu::ui::elm::TextBlock::New( hintText =
CardX + space::lg, pu::ui::elm::TextBlock::New(CardX + space::lg, CardY + CardH - space::lg - 18, "B to cancel");
CardY + CardH - space::lg - 18,
"B to cancel");
hintText->SetFont(type::font(type::Caption)); hintText->SetFont(type::font(type::Caption));
hintText->SetColor(color::TextMuted); hintText->SetColor(color::TextMuted);
@@ -91,4 +81,4 @@ namespace ui {
} }
}; };
} } // namespace ui
+4 -2
View File
@@ -1,7 +1,9 @@
#pragma once #pragma once
#include <string>
#include <optional> #include <optional>
#include <string>
#include <switch.h> #include <switch.h>
#include <nxst/domain/account.hpp> #include <nxst/domain/account.hpp>
namespace ui { namespace ui {
@@ -9,4 +11,4 @@ namespace ui {
std::optional<AccountUid> selectedUser; std::optional<AccountUid> selectedUser;
std::string selectedUserName; std::string selectedUserName;
}; };
} } // namespace ui
+4 -5
View File
@@ -1,14 +1,15 @@
#include <memory>
#include <pu/Plutonium> #include <pu/Plutonium>
#include <nxst/ui/const.h> #include <nxst/ui/const.h>
#include <nxst/ui/header_bar.hpp> #include <nxst/ui/header_bar.hpp>
#include <nxst/ui/hint_bar.hpp> #include <nxst/ui/hint_bar.hpp>
#include <memory>
namespace ui { namespace ui {
class UsersLayout : public pu::ui::Layout { class UsersLayout : public pu::ui::Layout {
private: private:
pu::ui::elm::Menu::Ref usersMenu; pu::ui::elm::Menu::Ref usersMenu;
pu::ui::elm::Rectangle::Ref loadingBg; pu::ui::elm::Rectangle::Ref loadingBg;
pu::ui::elm::TextBlock::Ref loadingText; pu::ui::elm::TextBlock::Ref loadingText;
@@ -16,7 +17,6 @@ namespace ui {
std::unique_ptr<HintBar> hints; std::unique_ptr<HintBar> hints;
public: public:
UsersLayout(); UsersLayout();
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos); void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
@@ -24,6 +24,5 @@ namespace ui {
int32_t GetCurrentIndex(); int32_t GetCurrentIndex();
PU_SMART_CTOR(UsersLayout) PU_SMART_CTOR(UsersLayout)
}; };
} } // namespace ui
+12 -9
View File
@@ -1,9 +1,12 @@
#include <nxst/app/main_application.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/app/main.hpp>
#include <unistd.h> #include <unistd.h>
namespace ui { extern MainApplication* mainApp; } #include <nxst/app/main.hpp>
#include <nxst/app/main_application.hpp>
#include <nxst/domain/util.hpp>
namespace ui {
extern MainApplication* mainApp;
}
static int nxlink_sock = -1; static int nxlink_sock = -1;
@@ -25,9 +28,9 @@ extern "C" void userAppExit() {
if (ui::mainApp) { if (ui::mainApp) {
ui::mainApp->transfer.cancelReceive(); ui::mainApp->transfer.cancelReceive();
ui::mainApp->transfer.cancelSend(); ui::mainApp->transfer.cancelSend();
for (int i = 0; i < 150 && for (int i = 0; i < 150 && (!ui::mainApp->transfer.isReceiveWorkersIdle() ||
(!ui::mainApp->transfer.isReceiveWorkersIdle() || !ui::mainApp->transfer.isSendWorkersIdle());
!ui::mainApp->transfer.isSendWorkersIdle()); i++) { i++) {
usleep(10000); usleep(10000);
} }
} }
@@ -57,8 +60,8 @@ int main() {
// First create our renderer, where one can customize SDL or other stuff's // First create our renderer, where one can customize SDL or other stuff's
// initialization. // initialization.
auto renderer_opts = pu::ui::render::RendererInitOptions( auto renderer_opts =
SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); pu::ui::render::RendererInitOptions(SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags);
renderer_opts.UseImage(pu::ui::render::IMGAllFlags); renderer_opts.UseImage(pu::ui::render::IMGAllFlags);
renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); renderer_opts.UseAudio(pu::ui::render::MixerAllFlags);
renderer_opts.UseTTF(); renderer_opts.UseTTF();
+9 -6
View File
@@ -1,7 +1,9 @@
#include <string> #include <string>
#include <switch.h>
#include <switch/services/hid.h> #include <switch/services/hid.h>
#include <vector> #include <vector>
#include <switch.h>
#include <nxst/app/main_application.hpp> #include <nxst/app/main_application.hpp>
namespace ui { namespace ui {
@@ -11,11 +13,12 @@ namespace ui {
mainApp = this; mainApp = this;
this->users_layout = UsersLayout::New(); this->users_layout = UsersLayout::New();
this->titles_layout = TitlesLayout::New(); this->titles_layout = TitlesLayout::New();
this->users_layout->SetOnInput( this->users_layout->SetOnInput(std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1,
std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1, std::placeholders::_2, std::placeholders::_2, std::placeholders::_3,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_4));
this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout, std::placeholders::_1, std::placeholders::_2, this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout,
std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->LoadLayout(this->users_layout); this->LoadLayout(this->users_layout);
} }
} } // namespace ui
+23 -24
View File
@@ -24,16 +24,17 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include <nxst/domain/account.hpp>
#include <sys/stat.h>
#include <cstdio> #include <cstdio>
#include <sys/stat.h>
#include <nxst/domain/account.hpp>
static std::map<AccountUid, User> mUsers; static std::map<AccountUid, User> mUsers;
Result Account::init(void) Result Account::init(void) {
{
Result res = accountInitialize(AccountServiceType_Application); Result res = accountInitialize(AccountServiceType_Application);
if (R_FAILED(res)) return res; if (R_FAILED(res))
return res;
AccountUid uids[8]; AccountUid uids[8];
s32 count = 0; s32 count = 0;
@@ -44,13 +45,11 @@ Result Account::init(void)
return 0; return 0;
} }
void Account::exit(void) void Account::exit(void) {
{
accountExit(); accountExit();
} }
std::vector<AccountUid> Account::ids(void) std::vector<AccountUid> Account::ids(void) {
{
std::vector<AccountUid> v; std::vector<AccountUid> v;
for (auto& value : mUsers) { for (auto& value : mUsers) {
v.push_back(value.second.id); v.push_back(value.second.id);
@@ -58,8 +57,7 @@ std::vector<AccountUid> Account::ids(void)
return v; return v;
} }
static User getUser(AccountUid id) static User getUser(AccountUid id) {
{
User user{id, ""}; User user{id, ""};
AccountProfile profile; AccountProfile profile;
AccountProfileBase profilebase; AccountProfileBase profilebase;
@@ -74,8 +72,7 @@ static User getUser(AccountUid id)
return user; return user;
} }
std::string Account::username(AccountUid id) std::string Account::username(AccountUid id) {
{
std::map<AccountUid, User>::const_iterator got = mUsers.find(id); std::map<AccountUid, User>::const_iterator got = mUsers.find(id);
if (got == mUsers.end()) { if (got == mUsers.end()) {
User user = getUser(id); User user = getUser(id);
@@ -86,21 +83,21 @@ std::string Account::username(AccountUid id)
return got->second.name; return got->second.name;
} }
std::string Account::iconPath(AccountUid id) std::string Account::iconPath(AccountUid id) {
{
char path[128]; char path[128];
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg", id.uid[0], id.uid[1]);
id.uid[0], id.uid[1]);
struct stat st; struct stat st;
if (stat(path, &st) == 0 && st.st_size > 0) return std::string(path); if (stat(path, &st) == 0 && st.st_size > 0)
return std::string(path);
mkdir("sdmc:/switch", 0755); mkdir("sdmc:/switch", 0755);
mkdir("sdmc:/switch/NXST", 0755); mkdir("sdmc:/switch/NXST", 0755);
mkdir("sdmc:/switch/NXST/cache", 0755); mkdir("sdmc:/switch/NXST/cache", 0755);
AccountProfile profile; AccountProfile profile;
if (R_FAILED(accountGetProfile(&profile, id))) return ""; if (R_FAILED(accountGetProfile(&profile, id)))
return "";
u32 imgSize = 0; u32 imgSize = 0;
if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) { if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) {
@@ -112,24 +109,26 @@ std::string Account::iconPath(AccountUid id)
u32 outSize = 0; u32 outSize = 0;
Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize); Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize);
accountProfileClose(&profile); accountProfileClose(&profile);
if (R_FAILED(r) || outSize == 0) return ""; if (R_FAILED(r) || outSize == 0)
return "";
FILE* f = fopen(path, "wb"); FILE* f = fopen(path, "wb");
if (!f) return ""; if (!f)
return "";
fwrite(buf.data(), 1, outSize, f); fwrite(buf.data(), 1, outSize, f);
fclose(f); fclose(f);
return std::string(path); return std::string(path);
} }
AccountUid Account::selectAccount(void) AccountUid Account::selectAccount(void) {
{
LibAppletArgs args; LibAppletArgs args;
libappletArgsCreate(&args, 0x10000); libappletArgsCreate(&args, 0x10000);
u8 st_in[0xA0] = {0}; u8 st_in[0xA0] = {0};
u8 st_out[0x18] = {0}; u8 st_out[0x18] = {0};
size_t repsz; size_t repsz;
Result res = libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz); Result res =
libappletLaunch(AppletId_LibraryAppletPlayerSelect, &args, st_in, 0xA0, st_out, 0x18, &repsz);
if (R_SUCCEEDED(res)) { if (R_SUCCEEDED(res)) {
u64 lres = *(u64*)st_out; u64 lres = *(u64*)st_out;
AccountUid uid = *(AccountUid*)&st_out[8]; AccountUid uid = *(AccountUid*)&st_out[8];
+30 -33
View File
@@ -26,8 +26,7 @@
#include <nxst/domain/common.hpp> #include <nxst/domain/common.hpp>
std::string DateTime::timeStr(void) std::string DateTime::timeStr(void) {
{
time_t unixTime; time_t unixTime;
struct tm timeStruct; struct tm timeStruct;
time(&unixTime); time(&unixTime);
@@ -35,35 +34,32 @@ std::string DateTime::timeStr(void)
return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec); return StringUtils::format("%02i:%02i:%02i", timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
} }
std::string DateTime::dateTimeStr(void) std::string DateTime::dateTimeStr(void) {
{
time_t unixTime; time_t unixTime;
struct tm timeStruct; struct tm timeStruct;
time(&unixTime); time(&unixTime);
localtime_r(&unixTime, &timeStruct); localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour, return StringUtils::format("%04i%02i%02i-%02i%02i%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1,
timeStruct.tm_mday, timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
}
std::string DateTime::logDateTime(void) {
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900,
timeStruct.tm_mon + 1, timeStruct.tm_mday, timeStruct.tm_hour,
timeStruct.tm_min, timeStruct.tm_sec); timeStruct.tm_min, timeStruct.tm_sec);
} }
std::string DateTime::logDateTime(void) std::string StringUtils::UTF16toUTF8(const std::u16string& src) {
{
time_t unixTime;
struct tm timeStruct;
time(&unixTime);
localtime_r(&unixTime, &timeStruct);
return StringUtils::format("%04i-%02i-%02i %02i:%02i:%02i", timeStruct.tm_year + 1900, timeStruct.tm_mon + 1, timeStruct.tm_mday,
timeStruct.tm_hour, timeStruct.tm_min, timeStruct.tm_sec);
}
std::string StringUtils::UTF16toUTF8(const std::u16string& src)
{
static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert; static std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
std::string dst = convert.to_bytes(src); std::string dst = convert.to_bytes(src);
return dst; return dst;
} }
std::string StringUtils::removeForbiddenCharacters(std::string src) std::string StringUtils::removeForbiddenCharacters(std::string src) {
{
static const std::string illegalChars = ".,!\\/:?*\"<>|"; static const std::string illegalChars = ".,!\\/:?*\"<>|";
for (size_t i = 0, sz = src.length(); i < sz; i++) { for (size_t i = 0, sz = src.length(); i < sz; i++) {
if (illegalChars.find(src[i]) != std::string::npos) { if (illegalChars.find(src[i]) != std::string::npos) {
@@ -79,8 +75,7 @@ std::string StringUtils::removeForbiddenCharacters(std::string src)
return src; return src;
} }
std::string StringUtils::format(const std::string fmt_str, ...) std::string StringUtils::format(const std::string fmt_str, ...) {
{
va_list ap; va_list ap;
char* fp = NULL; char* fp = NULL;
va_start(ap, fmt_str); va_start(ap, fmt_str);
@@ -90,8 +85,7 @@ std::string StringUtils::format(const std::string fmt_str, ...)
return std::string(formatted.get()); return std::string(formatted.get());
} }
bool StringUtils::containsInvalidChar(const std::string& str) bool StringUtils::containsInvalidChar(const std::string& str) {
{
for (size_t i = 0, sz = str.length(); i < sz; i++) { for (size_t i = 0, sz = str.length(); i < sz; i++) {
if (!isascii(str[i])) { if (!isascii(str[i])) {
return true; return true;
@@ -100,24 +94,27 @@ bool StringUtils::containsInvalidChar(const std::string& str)
return false; return false;
} }
void StringUtils::ltrim(std::string& s) void StringUtils::ltrim(std::string& s) {
{ s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { return !std::isspace(ch); })); return !std::isspace(ch);
}));
} }
void StringUtils::rtrim(std::string& s) void StringUtils::rtrim(std::string& s) {
{ s.erase(std::find_if(s.rbegin(), s.rend(),
s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { return !std::isspace(ch); }).base(), s.end()); [](int ch) {
return !std::isspace(ch);
})
.base(),
s.end());
} }
void StringUtils::trim(std::string& s) void StringUtils::trim(std::string& s) {
{
ltrim(s); ltrim(s);
rtrim(s); rtrim(s);
} }
char* getConsoleIP(void) char* getConsoleIP(void) {
{
struct in_addr in; struct in_addr in;
in.s_addr = gethostid(); in.s_addr = gethostid();
return inet_ntoa(in); return inet_ntoa(in);
+44 -67
View File
@@ -24,26 +24,26 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include <nxst/domain/title.hpp>
#include <nxst/app/main.hpp> #include <nxst/app/main.hpp>
#include <nxst/domain/title.hpp>
static std::unordered_map<AccountUid, std::vector<Title>> titles; static std::unordered_map<AccountUid, std::vector<Title>> titles;
static bool s_titlesLoaded = false; static bool s_titlesLoaded = false;
bool areTitlesLoaded(void) bool areTitlesLoaded(void) {
{
return s_titlesLoaded; return s_titlesLoaded;
} }
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, const std::string& author) void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name,
{ const std::string& author) {
mId = id; mId = id;
mUserId = userID; mUserId = userID;
mSaveDataType = saveDataType; mSaveDataType = saveDataType;
mUserName = Account::username(userID); mUserName = Account::username(userID);
mAuthor = author; mAuthor = author;
mName = name; mName = name;
mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId) : StringUtils::removeForbiddenCharacters(name); mSafeName = StringUtils::containsInvalidChar(name) ? StringUtils::format("0x%016llX", mId)
: StringUtils::removeForbiddenCharacters(name);
mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName; mPath = "sdmc:/switch/NXST/saves/" + StringUtils::format("0x%016llX", mId) + " " + mSafeName;
std::string aname = StringUtils::removeAccents(mName); std::string aname = StringUtils::removeAccents(mName);
@@ -56,8 +56,7 @@ void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string&
StringUtils::trim(name2); StringUtils::trim(name2);
mDisplayName.first = name1; mDisplayName.first = name1;
mDisplayName.second = name2; mDisplayName.second = name2;
} } else {
else {
// check for parenthesis // check for parenthesis
size_t pos1 = aname.rfind("("); size_t pos1 = aname.rfind("(");
size_t pos2 = aname.rfind(")"); size_t pos2 = aname.rfind(")");
@@ -74,94 +73,77 @@ void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string&
refreshDirectories(); refreshDirectories();
} }
u8 Title::saveDataType(void) u8 Title::saveDataType(void) {
{
return mSaveDataType; return mSaveDataType;
} }
u64 Title::id(void) u64 Title::id(void) {
{
return mId; return mId;
} }
u64 Title::saveId(void) u64 Title::saveId(void) {
{
return mSaveId; return mSaveId;
} }
void Title::saveId(u64 saveId) void Title::saveId(u64 saveId) {
{
mSaveId = saveId; mSaveId = saveId;
} }
AccountUid Title::userId(void) AccountUid Title::userId(void) {
{
return mUserId; return mUserId;
} }
std::string Title::userName(void) std::string Title::userName(void) {
{
return mUserName; return mUserName;
} }
std::string Title::author(void) std::string Title::author(void) {
{
return mAuthor; return mAuthor;
} }
std::string Title::name(void) std::string Title::name(void) {
{
return mName; return mName;
} }
std::pair<std::string, std::string> Title::displayName(void) std::pair<std::string, std::string> Title::displayName(void) {
{
return mDisplayName; return mDisplayName;
} }
std::string Title::path(void) std::string Title::path(void) {
{
return mPath; return mPath;
} }
std::string Title::fullPath(size_t index) std::string Title::fullPath(size_t index) {
{
return mFullSavePaths.at(index); return mFullSavePaths.at(index);
} }
std::vector<std::string> Title::saves() std::vector<std::string> Title::saves() {
{
return mSaves; return mSaves;
} }
u64 Title::playTimeNanoseconds(void) u64 Title::playTimeNanoseconds(void) {
{
return mPlayTimeNanoseconds; return mPlayTimeNanoseconds;
} }
std::string Title::playTime(void) std::string Title::playTime(void) {
{
const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000; const u64 playTimeMinutes = mPlayTimeNanoseconds / 60000000000;
return StringUtils::format("%d", playTimeMinutes / 60) + ":" + StringUtils::format("%02d", playTimeMinutes % 60) + " hours"; return StringUtils::format("%d", playTimeMinutes / 60) + ":" +
StringUtils::format("%02d", playTimeMinutes % 60) + " hours";
} }
void Title::playTimeNanoseconds(u64 playTimeNanoseconds) void Title::playTimeNanoseconds(u64 playTimeNanoseconds) {
{
mPlayTimeNanoseconds = playTimeNanoseconds; mPlayTimeNanoseconds = playTimeNanoseconds;
} }
u32 Title::lastPlayedTimestamp(void) u32 Title::lastPlayedTimestamp(void) {
{
return mLastPlayedTimestamp; return mLastPlayedTimestamp;
} }
void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) void Title::lastPlayedTimestamp(u32 lastPlayedTimestamp) {
{
mLastPlayedTimestamp = lastPlayedTimestamp; mLastPlayedTimestamp = lastPlayedTimestamp;
} }
void Title::refreshDirectories(void) void Title::refreshDirectories(void) {
{
mSaves.clear(); mSaves.clear();
mFullSavePaths.clear(); mFullSavePaths.clear();
@@ -178,15 +160,15 @@ void Title::refreshDirectories(void)
std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend()); std::sort(mFullSavePaths.rbegin(), mFullSavePaths.rend());
mSaves.insert(mSaves.begin(), g_emptySave); mSaves.insert(mSaves.begin(), g_emptySave);
mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave); mFullSavePaths.insert(mFullSavePaths.begin(), g_emptySave);
} } else {
else { Logger::getInstance().log(Logger::ERROR,
Logger::getInstance().log(Logger::ERROR, "Couldn't retrieve the extdata directory list for the title " + name()); "Couldn't retrieve the extdata directory list for the title " + name());
} }
} }
void loadTitles(void) void loadTitles(void) {
{ if (s_titlesLoaded)
if (s_titlesLoaded) return; return;
s_titlesLoaded = true; s_titlesLoaded = true;
titles.clear(); titles.clear();
@@ -220,12 +202,14 @@ void loadTitles(void)
u64 sid = info.save_data_id; u64 sid = info.save_data_id;
AccountUid uid = info.uid; AccountUid uid = info.uid;
// if (mFilterIds.find(tid) == mFilterIds.end()) { // if (mFilterIds.find(tid) == mFilterIds.end()) {
res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd, sizeof(NsApplicationControlData), &outsize); res = nsGetApplicationControlData(NsApplicationControlSource_Storage, tid, nsacd,
sizeof(NsApplicationControlData), &outsize);
if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) { if (R_SUCCEEDED(res) && !(outsize < sizeof(nsacd->nacp))) {
res = nacpGetLanguageEntry(&nsacd->nacp, &nle); res = nacpGetLanguageEntry(&nsacd->nacp, &nle);
if (R_SUCCEEDED(res) && nle != NULL) { if (R_SUCCEEDED(res) && nle != NULL) {
Title title; Title title;
title.init(info.save_data_type, tid, uid, std::string(nle->name), std::string(nle->author)); title.init(info.save_data_type, tid, uid, std::string(nle->name),
std::string(nle->author));
title.saveId(sid); title.saveId(sid);
// load play statistics // load play statistics
@@ -243,8 +227,7 @@ void loadTitles(void)
if (it != titles.end()) { if (it != titles.end()) {
// found // found
it->second.push_back(title); it->second.push_back(title);
} } else {
else {
// not found, insert into map // not found, insert into map
std::vector<Title> v; std::vector<Title> v;
v.push_back(title); v.push_back(title);
@@ -263,8 +246,7 @@ void loadTitles(void)
sortTitles(); sortTitles();
} }
void sortTitles(void) void sortTitles(void) {
{
for (auto& vect : titles) { for (auto& vect : titles) {
std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) { std::sort(vect.second.begin(), vect.second.end(), [](Title& l, Title& r) {
switch (g_sortMode) { switch (g_sortMode) {
@@ -280,28 +262,24 @@ void sortTitles(void)
} }
} }
void rotateSortMode(void) void rotateSortMode(void) {
{
g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT); g_sortMode = static_cast<sort_t>((g_sortMode + 1) % SORT_MODES_COUNT);
sortTitles(); sortTitles();
} }
void getTitle(Title& dst, AccountUid uid, size_t i) void getTitle(Title& dst, AccountUid uid, size_t i) {
{
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
if (it != titles.end() && i < getTitleCount(uid)) { if (it != titles.end() && i < getTitleCount(uid)) {
dst = it->second.at(i); dst = it->second.at(i);
} }
} }
size_t getTitleCount(AccountUid uid) size_t getTitleCount(AccountUid uid) {
{
std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid); std::unordered_map<AccountUid, std::vector<Title>>::iterator it = titles.find(uid);
return it != titles.end() ? it->second.size() : 0; return it != titles.end() ? it->second.size() : 0;
} }
void refreshDirectories(u64 id) void refreshDirectories(u64 id) {
{
for (auto& pair : titles) { for (auto& pair : titles) {
for (size_t i = 0; i < pair.second.size(); i++) { for (size_t i = 0; i < pair.second.size(); i++) {
if (pair.second.at(i).id() == id) { if (pair.second.at(i).id() == id) {
@@ -311,8 +289,7 @@ void refreshDirectories(u64 id)
} }
} }
std::unordered_map<std::string, std::string> getCompleteTitleList(void) std::unordered_map<std::string, std::string> getCompleteTitleList(void) {
{
std::unordered_map<std::string, std::string> map; std::unordered_map<std::string, std::string> map;
for (const auto& pair : titles) { for (const auto& pair : titles) {
for (auto value : pair.second) { for (auto value : pair.second) {
+19 -25
View File
@@ -24,21 +24,19 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include <nxst/app/main.hpp>
#include <nxst/app/main_application.hpp>
#include <nxst/domain/util.hpp> #include <nxst/domain/util.hpp>
#include <nxst/infra/sys/logger.hpp> #include <nxst/infra/sys/logger.hpp>
#include <nxst/app/main_application.hpp>
#include <nxst/app/main.hpp>
void servicesExit(void) void servicesExit(void) {
{
Logger::getInstance().flush(); Logger::getInstance().flush();
Account::exit(); Account::exit();
plExit(); plExit();
romfsExit(); romfsExit();
} }
Result servicesInit(void) Result servicesInit(void) {
{
io::createDirectory("sdmc:/switch"); io::createDirectory("sdmc:/switch");
io::createDirectory("sdmc:/switch/NXST"); io::createDirectory("sdmc:/switch/NXST");
io::createDirectory("sdmc:/switch/NXST/saves"); io::createDirectory("sdmc:/switch/NXST/saves");
@@ -71,30 +69,28 @@ Result servicesInit(void)
if (R_SUCCEEDED(res = hidsysInitialize())) { if (R_SUCCEEDED(res = hidsysInitialize())) {
g_notificationLedAvailable = true; g_notificationLedAvailable = true;
} } else {
else {
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res); Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
} }
Logger::getInstance().log(Logger::INFO, "NXST loading completed!"); Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
return 0; return 0;
} }
std::u16string StringUtils::UTF8toUTF16(const char* src) std::u16string StringUtils::UTF8toUTF16(const char* src) {
{
char16_t tmp[256] = {0}; char16_t tmp[256] = {0};
utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256); utf8_to_utf16((uint16_t*)tmp, (uint8_t*)src, 256);
return std::u16string(tmp); return std::u16string(tmp);
} }
// https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c // https://stackoverflow.com/questions/14094621/change-all-accented-letters-to-normal-letters-in-c
std::string StringUtils::removeAccents(std::string str) std::string StringUtils::removeAccents(std::string str) {
{
std::u16string src = UTF8toUTF16(str.c_str()); std::u16string src = UTF8toUTF16(str.c_str());
const std::u16string illegal = UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ"); const std::u16string illegal =
const std::u16string fixed = UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy"); UTF8toUTF16("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüūýþÿ");
const std::u16string fixed =
UTF8toUTF16("AAAAAAECEEEEIIIIDNOOOOOx0UUUUYPsaaaaaaeceeeeiiiiOnooooo/0uuuuuypy");
for (size_t i = 0, sz = src.length(); i < sz; i++) { for (size_t i = 0, sz = src.length(); i < sz; i++) {
size_t index = illegal.find(src[i]); size_t index = illegal.find(src[i]);
@@ -106,8 +102,7 @@ std::string StringUtils::removeAccents(std::string str)
return UTF16toUTF8(src); return UTF16toUTF8(src);
} }
std::string StringUtils::removeNotAscii(std::string str) std::string StringUtils::removeNotAscii(std::string str) {
{
for (size_t i = 0, sz = str.length(); i < sz; i++) { for (size_t i = 0, sz = str.length(); i < sz; i++) {
if (!isascii(str[i])) { if (!isascii(str[i])) {
str[i] = ' '; str[i] = ' ';
@@ -116,9 +111,9 @@ std::string StringUtils::removeNotAscii(std::string str)
return str; return str;
} }
std::string StringUtils::elide(const std::string& s, size_t maxChars) std::string StringUtils::elide(const std::string& s, size_t maxChars) {
{ if (s.size() <= maxChars || maxChars < 6)
if (s.size() <= maxChars || maxChars < 6) return s; return s;
constexpr const char* dots = "..."; constexpr const char* dots = "...";
size_t budget = maxChars - 3; size_t budget = maxChars - 3;
size_t head = (budget + 1) / 2; size_t head = (budget + 1) / 2;
@@ -126,8 +121,7 @@ std::string StringUtils::elide(const std::string& s, size_t maxChars)
return s.substr(0, head) + dots + s.substr(s.size() - tail); return s.substr(0, head) + dots + s.substr(s.size() - tail);
} }
HidsysNotificationLedPattern blinkLedPattern(u8 times) HidsysNotificationLedPattern blinkLedPattern(u8 times) {
{
HidsysNotificationLedPattern pattern; HidsysNotificationLedPattern pattern;
memset(&pattern, 0, sizeof(pattern)); memset(&pattern, 0, sizeof(pattern));
@@ -146,8 +140,7 @@ HidsysNotificationLedPattern blinkLedPattern(u8 times)
return pattern; return pattern;
} }
void blinkLed(u8 times) void blinkLed(u8 times) {
{
if (g_notificationLedAvailable) { if (g_notificationLedAvailable) {
PadState pad; PadState pad;
padInitializeDefault(&pad); padInitializeDefault(&pad);
@@ -155,7 +148,8 @@ void blinkLed(u8 times)
HidsysUniquePadId uniquePadIds[2] = {0}; HidsysUniquePadId uniquePadIds[2] = {0};
HidsysNotificationLedPattern pattern = blinkLedPattern(times); HidsysNotificationLedPattern pattern = blinkLedPattern(times);
memset(uniquePadIds, 0, sizeof(uniquePadIds)); memset(uniquePadIds, 0, sizeof(uniquePadIds));
Result res = hidsysGetUniquePadsFromNpad(padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n); Result res = hidsysGetUniquePadsFromNpad(
padIsHandheld(&pad) ? HidNpadIdType_Handheld : HidNpadIdType_No1, uniquePadIds, 2, &n);
if (R_SUCCEEDED(res)) { if (R_SUCCEEDED(res)) {
for (s32 i = 0; i < n; i++) { for (s32 i = 0; i < n; i++) {
hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]); hidsysSetNotificationLedPattern(&pattern, uniquePadIds[i]);
+7 -14
View File
@@ -26,8 +26,7 @@
#include <nxst/infra/fs/directory.hpp> #include <nxst/infra/fs/directory.hpp>
Directory::Directory(const std::string& root) Directory::Directory(const std::string& root) {
{
mGood = false; mGood = false;
mError = 0; mError = 0;
mList.clear(); mList.clear();
@@ -37,8 +36,7 @@ Directory::Directory(const std::string& root)
if (dir == NULL) { if (dir == NULL) {
mError = (Result)errno; mError = (Result)errno;
} } else {
else {
while ((ent = readdir(dir))) { while ((ent = readdir(dir))) {
std::string name = std::string(ent->d_name); std::string name = std::string(ent->d_name);
bool directory = ent->d_type == DT_DIR; bool directory = ent->d_type == DT_DIR;
@@ -50,27 +48,22 @@ Directory::Directory(const std::string& root)
} }
} }
Result Directory::error(void) Result Directory::error(void) {
{
return mError; return mError;
} }
bool Directory::good(void) bool Directory::good(void) {
{
return mGood; return mGood;
} }
std::string Directory::entry(size_t index) std::string Directory::entry(size_t index) {
{
return index < mList.size() ? mList.at(index).name : ""; return index < mList.size() ? mList.at(index).name : "";
} }
bool Directory::folder(size_t index) bool Directory::folder(size_t index) {
{
return index < mList.size() ? mList.at(index).directory : false; return index < mList.size() ? mList.at(index).directory : false;
} }
size_t Directory::size(void) size_t Directory::size(void) {
{
return mList.size(); return mList.size();
} }
+3 -6
View File
@@ -26,17 +26,14 @@
#include <nxst/infra/fs/filesystem.hpp> #include <nxst/infra/fs/filesystem.hpp>
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) {
{
return fsOpen_SaveData(fileSystem, titleID, userID); return fsOpen_SaveData(fileSystem, titleID, userID);
} }
int FileSystem::mount(FsFileSystem fs) int FileSystem::mount(FsFileSystem fs) {
{
return fsdevMountDevice("save", fs); return fsdevMountDevice("save", fs);
} }
void FileSystem::unmount(void) void FileSystem::unmount(void) {
{
fsdevUnmountDevice("save"); fsdevUnmountDevice("save");
} }
+26 -39
View File
@@ -24,20 +24,19 @@
* reasonable ways as different from the original version. * reasonable ways as different from the original version.
*/ */
#include <nxst/infra/fs/io.hpp>
#include <nxst/infra/fs/handles.hpp>
#include <nxst/app/main.hpp>
#include <nxst/infra/sys/logger.hpp>
#include <vector> #include <vector>
bool io::fileExists(const std::string& path) #include <nxst/app/main.hpp>
{ #include <nxst/infra/fs/handles.hpp>
#include <nxst/infra/fs/io.hpp>
#include <nxst/infra/sys/logger.hpp>
bool io::fileExists(const std::string& path) {
struct stat buffer; struct stat buffer;
return (stat(path.c_str(), &buffer) == 0); return (stat(path.c_str(), &buffer) == 0);
} }
void io::copyFile(const std::string& srcPath, const std::string& dstPath) void io::copyFile(const std::string& srcPath, const std::string& dstPath) {
{
g_isTransferringFile = true; g_isTransferringFile = true;
nxst::FileHandle src(fopen(srcPath.c_str(), "rb")); nxst::FileHandle src(fopen(srcPath.c_str(), "rb"));
@@ -69,8 +68,7 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath)
u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get()); u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get());
if (count == 0) { if (count == 0) {
nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.", nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.",
srcPath.c_str(), (unsigned long long)offset, srcPath.c_str(), (unsigned long long)offset, (unsigned long long)sz, errno);
(unsigned long long)sz, errno);
break; break;
} }
fwrite(buf.data(), 1, count, dst.get()); fwrite(buf.data(), 1, count, dst.get());
@@ -84,8 +82,7 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath)
g_isTransferringFile = false; g_isTransferringFile = false;
} }
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath) {
{
Result res = 0; Result res = 0;
bool quit = false; bool quit = false;
Directory items(srcPath); Directory items(srcPath);
@@ -104,12 +101,10 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
newsrc += "/"; newsrc += "/";
newdst += "/"; newdst += "/";
res = io::copyDirectory(newsrc, newdst); res = io::copyDirectory(newsrc, newdst);
} } else {
else {
quit = true; quit = true;
} }
} } else {
else {
io::copyFile(newsrc, newdst); io::copyFile(newsrc, newdst);
} }
} }
@@ -117,20 +112,17 @@ Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
return 0; return 0;
} }
Result io::createDirectory(const std::string& path) Result io::createDirectory(const std::string& path) {
{
mkdir(path.c_str(), 0777); mkdir(path.c_str(), 0777);
return 0; return 0;
} }
bool io::directoryExists(const std::string& path) bool io::directoryExists(const std::string& path) {
{
struct stat sb; struct stat sb;
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode)); return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
} }
Result io::deleteFolderRecursively(const std::string& path) Result io::deleteFolderRecursively(const std::string& path) {
{
Directory dir(path); Directory dir(path);
if (!dir.good()) { if (!dir.good()) {
return dir.error(); return dir.error();
@@ -142,8 +134,7 @@ Result io::deleteFolderRecursively(const std::string& path)
deleteFolderRecursively(newpath); deleteFolderRecursively(newpath);
newpath = path + dir.entry(i); newpath = path + dir.entry(i);
rmdir(newpath.c_str()); rmdir(newpath.c_str());
} } else {
else {
std::string newpath = path + dir.entry(i); std::string newpath = path + dir.entry(i);
std::remove(newpath.c_str()); std::remove(newpath.c_str());
} }
@@ -153,14 +144,12 @@ Result io::deleteFolderRecursively(const std::string& path)
return 0; return 0;
} }
nxst::Result<std::string> io::backup(size_t index, AccountUid uid) nxst::Result<std::string> io::backup(size_t index, AccountUid uid) {
{
Title title; Title title;
getTitle(title, uid, index); getTitle(title, uid, index);
nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(),
title.name().c_str(), title.id(), title.id(), title.userId().uid[1], title.userId().uid[0]);
title.userId().uid[1], title.userId().uid[0]);
nxst::FsFileSystemHandle fsHandle; nxst::FsFileSystemHandle fsHandle;
Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId()); Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId());
@@ -180,7 +169,8 @@ nxst::Result<std::string> io::backup(size_t index, AccountUid uid)
} }
fsHandle.release(); // devfs now owns the kernel handle fsHandle.release(); // devfs now owns the kernel handle
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId()))); std::string suggestion =
StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
io::createDirectory(title.path()); io::createDirectory(title.path());
std::string dst_path = title.path() + "/" + suggestion; std::string dst_path = title.path() + "/" + suggestion;
@@ -215,14 +205,12 @@ nxst::Result<std::string> io::backup(size_t index, AccountUid uid)
} }
// Creates the save data filesystem for a title if it doesn't exist yet. // Creates the save data filesystem for a title if it doesn't exist yet.
static void createSaveIfNeeded(u64 title_id, AccountUid uid) static void createSaveIfNeeded(u64 title_id, AccountUid uid) {
{
std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0); std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0);
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data()); auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data());
size_t outsize = 0; size_t outsize = 0;
if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title_id, nsacd,
title_id, nsacd,
sizeof(NsApplicationControlData), &outsize))) { sizeof(NsApplicationControlData), &outsize))) {
return; return;
} }
@@ -245,16 +233,15 @@ static void createSaveIfNeeded(u64 title_id, AccountUid uid)
fsCreateSaveDataFileSystem(&attr, &create_info, &meta); fsCreateSaveDataFileSystem(&attr, &create_info, &meta);
} }
nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell) nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex,
{ const std::string& nameFromCell) {
(void)cellIndex; (void)cellIndex;
Title title; Title title;
getTitle(title, uid, index); getTitle(title, uid, index);
nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(),
title.name().c_str(), title.id(), title.id(), title.userId().uid[1], title.userId().uid[0]);
title.userId().uid[1], title.userId().uid[0]);
createSaveIfNeeded(title.id(), uid); createSaveIfNeeded(title.id(), uid);
+40 -14
View File
@@ -1,10 +1,10 @@
#include <nxst/infra/sys/logger.hpp>
#include <cstdarg> #include <cstdarg>
#include <cstdio> #include <cstdio>
#include <ctime> #include <ctime>
#include <mutex> #include <mutex>
#include <nxst/infra/sys/logger.hpp>
namespace { namespace {
std::mutex g_log_mutex; std::mutex g_log_mutex;
@@ -15,8 +15,7 @@ constexpr const char* kLogPath = "/switch/NXST/log.log";
constexpr const char* kLogPath = "nxst.log"; constexpr const char* kLogPath = "nxst.log";
#endif #endif
void writeEntry(const char* tag, const char* fmt, va_list args) void writeEntry(const char* tag, const char* fmt, va_list args) {
{
char msg[2048]; char msg[2048];
vsnprintf(msg, sizeof(msg), fmt, args); vsnprintf(msg, sizeof(msg), fmt, args);
@@ -41,14 +40,21 @@ void writeEntry(const char* tag, const char* fmt, va_list args)
namespace nxst::log { namespace nxst::log {
void write(Level level, const char* fmt, ...) void write(Level level, const char* fmt, ...) {
{
const char* tag = "[INFO] "; const char* tag = "[INFO] ";
switch (level) { switch (level) {
case Level::Debug: tag = "[DEBUG]"; break; case Level::Debug:
case Level::Info: tag = "[INFO] "; break; tag = "[DEBUG]";
case Level::Warn: tag = "[WARN] "; break; break;
case Level::Error: tag = "[ERROR]"; break; case Level::Info:
tag = "[INFO] ";
break;
case Level::Warn:
tag = "[WARN] ";
break;
case Level::Error:
tag = "[ERROR]";
break;
} }
va_list args; va_list args;
va_start(args, fmt); va_start(args, fmt);
@@ -56,9 +62,29 @@ void write(Level level, const char* fmt, ...)
va_end(args); va_end(args);
} }
void debug(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[DEBUG]", fmt, args); va_end(args); } void debug(const char* fmt, ...) {
void info (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[INFO] ", fmt, args); va_end(args); } va_list args;
void warn (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[WARN] ", fmt, args); va_end(args); } va_start(args, fmt);
void error(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[ERROR]", fmt, args); va_end(args); } writeEntry("[DEBUG]", fmt, args);
va_end(args);
}
void info(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[INFO] ", fmt, args);
va_end(args);
}
void warn(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[WARN] ", fmt, args);
va_end(args);
}
void error(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
writeEntry("[ERROR]", fmt, args);
va_end(args);
}
} // namespace nxst::log } // namespace nxst::log
+109 -52
View File
@@ -1,5 +1,3 @@
#include <nxst/service/transfer_service.hpp>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <chrono> #include <chrono>
#include <cstring> #include <cstring>
@@ -12,11 +10,14 @@
#include <unistd.h> #include <unistd.h>
#include <vector> #include <vector>
#include <nxst/service/transfer_service.hpp>
#ifdef __SWITCH__ #ifdef __SWITCH__
#include <switch.h> #include <switch.h>
#include <nxst/infra/fs/io.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/domain/account.hpp> #include <nxst/domain/account.hpp>
#include <nxst/domain/util.hpp>
#include <nxst/infra/fs/io.hpp>
#endif #endif
#include <nxst/domain/protocol.hpp> #include <nxst/domain/protocol.hpp>
@@ -32,7 +33,8 @@ static bool sendAll(int sock, const void* buf, size_t len) {
size_t sent = 0; size_t sent = 0;
while (sent < len) { while (sent < len) {
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0); ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
if (n <= 0) return false; if (n <= 0)
return false;
sent += n; sent += n;
} }
return true; return true;
@@ -42,7 +44,8 @@ static bool recvAll(int sock, void* buf, size_t len) {
size_t got = 0; size_t got = 0;
while (got < len) { while (got < len) {
ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got); ssize_t n = read(sock, static_cast<char*>(buf) + got, len - got);
if (n <= 0) return false; if (n <= 0)
return false;
got += n; got += n;
} }
return true; return true;
@@ -50,15 +53,19 @@ static bool recvAll(int sock, void* buf, size_t len) {
static bool sendFile(int sock, const fs::path& filepath, TransferState& state) { static bool sendFile(int sock, const fs::path& filepath, TransferState& state) {
std::ifstream infile(filepath, std::ios::binary | std::ios::ate); std::ifstream infile(filepath, std::ios::binary | std::ios::ate);
if (!infile.is_open()) return false; if (!infile.is_open())
return false;
uint32_t filename_len = (uint32_t)filepath.string().size(); uint32_t filename_len = (uint32_t)filepath.string().size();
uint64_t file_size = (uint64_t)infile.tellg(); uint64_t file_size = (uint64_t)infile.tellg();
infile.seekg(0, std::ios::beg); infile.seekg(0, std::ios::beg);
if (!sendAll(sock, &filename_len, sizeof(filename_len))) return false; if (!sendAll(sock, &filename_len, sizeof(filename_len)))
if (!sendAll(sock, filepath.c_str(), filename_len)) return false; return false;
if (!sendAll(sock, &file_size, sizeof(file_size))) return false; if (!sendAll(sock, filepath.c_str(), filename_len))
return false;
if (!sendAll(sock, &file_size, sizeof(file_size)))
return false;
std::vector<char> buffer(proto::BUF_SIZE); std::vector<char> buffer(proto::BUF_SIZE);
uint64_t remaining = file_size; uint64_t remaining = file_size;
@@ -66,8 +73,10 @@ static bool sendFile(int sock, const fs::path& filepath, TransferState& state) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
infile.read(buffer.data(), (std::streamsize)to_read); infile.read(buffer.data(), (std::streamsize)to_read);
std::streamsize count = infile.gcount(); std::streamsize count = infile.gcount();
if (count <= 0) break; if (count <= 0)
if (!sendAll(sock, buffer.data(), (size_t)count)) return false; break;
if (!sendAll(sock, buffer.data(), (size_t)count))
return false;
state.bytes_done.fetch_add((uint64_t)count); state.bytes_done.fetch_add((uint64_t)count);
remaining -= (uint64_t)count; remaining -= (uint64_t)count;
} }
@@ -84,12 +93,12 @@ static void mkdirs(const std::string& path) {
mkdir(path.c_str(), 0777); mkdir(path.c_str(), 0777);
} }
static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size, static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size, TransferState& state) {
TransferState& state) {
size_t last_slash = rel_path.rfind('/'); size_t last_slash = rel_path.rfind('/');
if (last_slash != std::string::npos) { if (last_slash != std::string::npos) {
std::string dir = rel_path.substr(0, last_slash); std::string dir = rel_path.substr(0, last_slash);
if (!dir.empty()) mkdirs(dir); if (!dir.empty())
mkdirs(dir);
} }
FILE* outfile = fopen(rel_path.c_str(), "wb"); FILE* outfile = fopen(rel_path.c_str(), "wb");
@@ -99,7 +108,8 @@ static void receiveFile(int sock, const std::string& rel_path, uint64_t file_siz
while (remaining > 0) { while (remaining > 0) {
size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, drain.data(), to_read); ssize_t n = read(sock, drain.data(), to_read);
if (n <= 0) break; if (n <= 0)
break;
remaining -= (uint64_t)n; remaining -= (uint64_t)n;
} }
return; return;
@@ -113,7 +123,8 @@ static void receiveFile(int sock, const std::string& rel_path, uint64_t file_siz
while (total < file_size) { while (total < file_size) {
size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::BUF_SIZE); size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::BUF_SIZE);
ssize_t n = read(sock, buffer.data(), to_read); ssize_t n = read(sock, buffer.data(), to_read);
if (n <= 0) break; if (n <= 0)
break;
fwrite(buffer.data(), 1, (size_t)n, outfile); fwrite(buffer.data(), 1, (size_t)n, outfile);
total += (uint64_t)n; total += (uint64_t)n;
state.bytes_done.store(total); state.bytes_done.store(total);
@@ -131,12 +142,14 @@ void TransferService::failSend(const std::string& reason) {
int TransferService::findServer(char* out_ip) { int TransferService::findServer(char* out_ip) {
int udp_fd = socket(AF_INET, SOCK_DGRAM, 0); int udp_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (udp_fd < 0) return -1; if (udp_fd < 0)
return -1;
sender_udp_sock.store(udp_fd); sender_udp_sock.store(udp_fd);
auto releaseUdp = [&]() { auto releaseUdp = [&]() {
int owned = sender_udp_sock.exchange(-1); int owned = sender_udp_sock.exchange(-1);
if (owned == udp_fd) close(udp_fd); if (owned == udp_fd)
close(udp_fd);
}; };
sockaddr_in addr{}; sockaddr_in addr{};
@@ -152,7 +165,10 @@ int TransferService::findServer(char* out_ip) {
// Poll in 100ms slices so cancel races within 100ms // Poll in 100ms slices so cancel races within 100ms
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
while (std::chrono::steady_clock::now() < deadline) { while (std::chrono::steady_clock::now() < deadline) {
if (sender_state.cancelled.load()) { releaseUdp(); return -1; } if (sender_state.cancelled.load()) {
releaseUdp();
return -1;
}
struct timeval tv{0, 100000}; struct timeval tv{0, 100000};
fd_set fds; fd_set fds;
FD_ZERO(&fds); FD_ZERO(&fds);
@@ -200,7 +216,8 @@ void TransferService::runSender(size_t title_index, AccountUid uid) {
failSend("No receiver found.\nMake sure the other Switch is in Receive mode."); failSend("No receiver found.\nMake sure the other Switch is in Receive mode.");
return finish(); return finish();
} }
if (sender_state.cancelled.load()) return finish(); if (sender_state.cancelled.load())
return finish();
sender_state.setStatus("Creating backup..."); sender_state.setStatus("Creating backup...");
#ifdef __SWITCH__ #ifdef __SWITCH__
@@ -212,19 +229,25 @@ void TransferService::runSender(size_t title_index, AccountUid uid) {
fs::path directory = backup_result.value(); fs::path directory = backup_result.value();
#else #else
fs::path directory = "."; fs::path directory = ".";
(void)title_index; (void)uid; (void)title_index;
(void)uid;
#endif #endif
if (sender_state.cancelled.load()) return finish(); if (sender_state.cancelled.load())
return finish();
sender_state.setStatus("Connecting..."); sender_state.setStatus("Connecting...");
int tcp_fd = socket(AF_INET, SOCK_STREAM, 0); int tcp_fd = socket(AF_INET, SOCK_STREAM, 0);
if (tcp_fd < 0) { failSend("Failed to open socket."); return finish(); } if (tcp_fd < 0) {
failSend("Failed to open socket.");
return finish();
}
sender_tcp_sock.store(tcp_fd); sender_tcp_sock.store(tcp_fd);
auto releaseTcp = [&]() { auto releaseTcp = [&]() {
int owned = sender_tcp_sock.exchange(-1); int owned = sender_tcp_sock.exchange(-1);
if (owned == tcp_fd) close(tcp_fd); if (owned == tcp_fd)
close(tcp_fd);
}; };
sockaddr_in serv{}; sockaddr_in serv{};
@@ -245,11 +268,13 @@ void TransferService::runSender(size_t title_index, AccountUid uid) {
sender_state.bytes_total.store(total); sender_state.bytes_total.store(total);
for (const auto& entry : fs::recursive_directory_iterator(directory)) { for (const auto& entry : fs::recursive_directory_iterator(directory)) {
if (sender_state.cancelled.load()) break; if (sender_state.cancelled.load())
break;
const fs::path& p = entry.path(); const fs::path& p = entry.path();
if (fs::is_regular_file(p)) { if (fs::is_regular_file(p)) {
sender_state.setStatus(p.filename().string()); sender_state.setStatus(p.filename().string());
if (!sendFile(tcp_fd, p, sender_state)) break; if (!sendFile(tcp_fd, p, sender_state))
break;
} }
} }
@@ -278,19 +303,26 @@ int TransferService::startSend(size_t title_index, AccountUid uid) {
void TransferService::cancelSend() { void TransferService::cancelSend() {
sender_state.cancelled.store(true); sender_state.cancelled.store(true);
int udp = sender_udp_sock.exchange(-1); int udp = sender_udp_sock.exchange(-1);
if (udp >= 0) { shutdown(udp, SHUT_RDWR); close(udp); } if (udp >= 0) {
shutdown(udp, SHUT_RDWR);
close(udp);
}
int tcp = sender_tcp_sock.exchange(-1); int tcp = sender_tcp_sock.exchange(-1);
if (tcp >= 0) { shutdown(tcp, SHUT_RDWR); close(tcp); } if (tcp >= 0) {
shutdown(tcp, SHUT_RDWR);
close(tcp);
}
} }
// ─── Receiver ──────────────────────────────────────────────────────────────── // ─── Receiver ────────────────────────────────────────────────────────────────
std::string TransferService::replaceUsername(const std::string& file_path) const { std::string TransferService::replaceUsername(const std::string& file_path) const {
#ifdef __SWITCH__ #ifdef __SWITCH__
std::string username = StringUtils::removeNotAscii( std::string username =
StringUtils::removeAccents(Account::username(restore_uid))); StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(restore_uid)));
size_t last_slash = file_path.rfind('/'); size_t last_slash = file_path.rfind('/');
if (last_slash == std::string::npos) return file_path; if (last_slash == std::string::npos)
return file_path;
size_t prev_slash = file_path.rfind('/', last_slash - 1); size_t prev_slash = file_path.rfind('/', last_slash - 1);
if (prev_slash == std::string::npos) if (prev_slash == std::string::npos)
return username + file_path.substr(last_slash); return username + file_path.substr(last_slash);
@@ -311,12 +343,16 @@ void TransferService::runBroadcast() {
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
int udp = socket(AF_INET, SOCK_DGRAM, 0); int udp = socket(AF_INET, SOCK_DGRAM, 0);
if (udp < 0) { receiver_broadcast_active.store(false); return; } if (udp < 0) {
receiver_broadcast_active.store(false);
return;
}
receiver_bcast_sock.store(udp); receiver_bcast_sock.store(udp);
auto releaseUdp = [&]() { auto releaseUdp = [&]() {
int owned = receiver_bcast_sock.exchange(-1); int owned = receiver_bcast_sock.exchange(-1);
if (owned == udp) close(udp); if (owned == udp)
close(udp);
}; };
struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit
@@ -348,7 +384,8 @@ void TransferService::runBroadcast() {
while (true) { while (true) {
ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen);
if (n < 0) { if (n < 0) {
if (receiver_state.cancelled.load()) break; if (receiver_state.cancelled.load())
break;
continue; continue;
} }
buf[n] = '\0'; buf[n] = '\0';
@@ -380,35 +417,42 @@ void TransferService::runAccept(int server_fd) {
int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len); int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len);
int owned_listen = receiver_listen_sock.exchange(-1); int owned_listen = receiver_listen_sock.exchange(-1);
if (owned_listen == server_fd) close(server_fd); if (owned_listen == server_fd)
close(server_fd);
if (client_sock >= 0) { if (client_sock >= 0) {
receiver_client_sock.store(client_sock); receiver_client_sock.store(client_sock);
while (true) { while (true) {
uint32_t filename_len = 0; uint32_t filename_len = 0;
if (!recvAll(client_sock, &filename_len, sizeof(filename_len))) break; if (!recvAll(client_sock, &filename_len, sizeof(filename_len)))
if (filename_len == proto::EOF_SENTINEL) break; break;
if (filename_len > proto::MAX_FILENAME) break; if (filename_len == proto::EOF_SENTINEL)
break;
if (filename_len > proto::MAX_FILENAME)
break;
std::vector<char> filename_buf(filename_len + 1, '\0'); std::vector<char> filename_buf(filename_len + 1, '\0');
if (!recvAll(client_sock, filename_buf.data(), filename_len)) break; if (!recvAll(client_sock, filename_buf.data(), filename_len))
break;
std::string filename_str(filename_buf.data(), filename_len); std::string filename_str(filename_buf.data(), filename_len);
filename_str = replaceUsername(filename_str); filename_str = replaceUsername(filename_str);
{ {
size_t sl = filename_str.rfind('/'); size_t sl = filename_str.rfind('/');
receiver_state.setStatus( receiver_state.setStatus(sl != std::string::npos ? filename_str.substr(sl + 1)
sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str); : filename_str);
} }
uint64_t file_size = 0; uint64_t file_size = 0;
if (!recvAll(client_sock, &file_size, sizeof(file_size))) break; if (!recvAll(client_sock, &file_size, sizeof(file_size)))
break;
receiveFile(client_sock, filename_str, file_size, receiver_state); receiveFile(client_sock, filename_str, file_size, receiver_state);
} }
int owned = receiver_client_sock.exchange(-1); int owned = receiver_client_sock.exchange(-1);
if (owned == client_sock) close(client_sock); if (owned == client_sock)
close(client_sock);
if (!receiver_state.cancelled.load()) { if (!receiver_state.cancelled.load()) {
#ifdef __SWITCH__ #ifdef __SWITCH__
@@ -436,12 +480,16 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin
restore_error.clear(); restore_error.clear();
pthread_t bcast_thread; pthread_t bcast_thread;
if (pthread_create(&bcast_thread, nullptr, broadcastEntry, this) != 0) return 1; if (pthread_create(&bcast_thread, nullptr, broadcastEntry, this) != 0)
return 1;
receiver_bcast_thread = bcast_thread; receiver_bcast_thread = bcast_thread;
pthread_detach(bcast_thread); pthread_detach(bcast_thread);
Socket server(socket(AF_INET, SOCK_STREAM, 0)); Socket server(socket(AF_INET, SOCK_STREAM, 0));
if (!server.valid()) { cancelReceive(); return 1; } if (!server.valid()) {
cancelReceive();
return 1;
}
int yes = 1; int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
@@ -451,8 +499,7 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin
addr.sin_addr.s_addr = INADDR_ANY; addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(proto::TCP_PORT); addr.sin_port = htons(proto::TCP_PORT);
if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || listen(server, 3) < 0) {
listen(server, 3) < 0) {
cancelReceive(); cancelReceive();
return 1; return 1;
} }
@@ -472,12 +519,22 @@ int TransferService::startReceive(size_t title_index, AccountUid uid, std::strin
void TransferService::cancelReceive() { void TransferService::cancelReceive() {
receiver_state.cancelled.store(true); receiver_state.cancelled.store(true);
int sock = receiver_client_sock.exchange(-1); int sock = receiver_client_sock.exchange(-1);
if (sock >= 0) { shutdown(sock, SHUT_RDWR); close(sock); } if (sock >= 0) {
shutdown(sock, SHUT_RDWR);
close(sock);
}
int lsock = receiver_listen_sock.exchange(-1); int lsock = receiver_listen_sock.exchange(-1);
if (lsock >= 0) { shutdown(lsock, SHUT_RDWR); close(lsock); } if (lsock >= 0) {
shutdown(lsock, SHUT_RDWR);
close(lsock);
}
int bsock = receiver_bcast_sock.exchange(-1); int bsock = receiver_bcast_sock.exchange(-1);
if (bsock >= 0) { shutdown(bsock, SHUT_RDWR); close(bsock); } if (bsock >= 0) {
if (receiver_broadcast_active.load()) pthread_cancel(receiver_bcast_thread); shutdown(bsock, SHUT_RDWR);
close(bsock);
}
if (receiver_broadcast_active.load())
pthread_cancel(receiver_bcast_thread);
} }
} // namespace nxst } // namespace nxst
+41 -39
View File
@@ -1,7 +1,7 @@
#include <nxst/app/main_application.hpp> #include <nxst/app/main_application.hpp>
#include <nxst/domain/util.hpp> #include <nxst/domain/util.hpp>
#include <nxst/ui/transfer_overlay.hpp>
#include <nxst/ui/const.h> #include <nxst/ui/const.h>
#include <nxst/ui/transfer_overlay.hpp>
namespace ui { namespace ui {
extern MainApplication* mainApp; extern MainApplication* mainApp;
@@ -15,75 +15,70 @@ namespace ui {
constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md; constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md;
constexpr int BtnH = 56; constexpr int BtnH = 56;
constexpr int BtnW = PanelW - 2 * theme::space::lg; constexpr int BtnW = PanelW - 2 * theme::space::lg;
} } // namespace
TitlesLayout::TitlesLayout() : Layout::Layout() { TitlesLayout::TitlesLayout() : Layout::Layout() {
using namespace theme; using namespace theme;
this->titlesMenu = pu::ui::elm::Menu::New( this->titlesMenu =
ListX, ContentY, ListW, pu::ui::elm::Menu::New(ListX, ContentY, ListW, color::BgBase, color::BgSurface2, 88, 6);
color::BgBase, color::BgSurface2,
88, 6);
this->titlesMenu->SetScrollbarColor(color::Primary); this->titlesMenu->SetScrollbarColor(color::Primary);
this->titlesMenu->SetItemsFocusColor(color::BgSurface2); this->titlesMenu->SetItemsFocusColor(color::BgSurface2);
this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); }); this->titlesMenu->SetOnSelectionChanged([this]() {
this->refreshPanel();
});
this->SetBackgroundColor(color::BgBase); this->SetBackgroundColor(color::BgBase);
this->Add(this->titlesMenu); this->Add(this->titlesMenu);
this->panelBg = pu::ui::elm::Rectangle::New( this->panelBg =
PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg); pu::ui::elm::Rectangle::New(PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg);
this->Add(this->panelBg); this->Add(this->panelBg);
this->panelTitle = pu::ui::elm::TextBlock::New( this->panelTitle = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg, "");
PanelX + space::lg, ContentY + space::lg, "");
this->panelTitle->SetFont(type::font(type::Title)); this->panelTitle->SetFont(type::font(type::Title));
this->panelTitle->SetColor(color::TextPrimary); this->panelTitle->SetColor(color::TextPrimary);
this->Add(this->panelTitle); this->Add(this->panelTitle);
this->panelHint = pu::ui::elm::TextBlock::New( this->panelHint =
PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:"); pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:");
this->panelHint->SetFont(type::font(type::Body)); this->panelHint->SetFont(type::font(type::Body));
this->panelHint->SetColor(color::TextSecondary); this->panelHint->SetColor(color::TextSecondary);
this->Add(this->panelHint); this->Add(this->panelHint);
int btnY = ContentY + 200; int btnY = ContentY + 200;
this->btnTransferBg = pu::ui::elm::Rectangle::New( this->btnTransferBg =
PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md); pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md);
this->Add(this->btnTransferBg); this->Add(this->btnTransferBg);
this->btnTransferText = pu::ui::elm::TextBlock::New( this->btnTransferText =
PanelX + space::lg + space::md, btnY + 14, "Transfer to another device"); pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY + 14, "Transfer to another device");
this->btnTransferText->SetFont(type::font(type::Body)); this->btnTransferText->SetFont(type::font(type::Body));
this->btnTransferText->SetColor(color::TextSecondary); this->btnTransferText->SetColor(color::TextSecondary);
this->Add(this->btnTransferText); this->Add(this->btnTransferText);
int btnY2 = btnY + BtnH + space::md; int btnY2 = btnY + BtnH + space::md;
this->btnReceiveBg = pu::ui::elm::Rectangle::New( this->btnReceiveBg =
PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md); pu::ui::elm::Rectangle::New(PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md);
this->Add(this->btnReceiveBg); this->Add(this->btnReceiveBg);
this->btnReceiveText = pu::ui::elm::TextBlock::New( this->btnReceiveText = pu::ui::elm::TextBlock::New(PanelX + space::lg + space::md, btnY2 + 14,
PanelX + space::lg + space::md, btnY2 + 14, "Receive from another device"); "Receive from another device");
this->btnReceiveText->SetFont(type::font(type::Body)); this->btnReceiveText->SetFont(type::font(type::Body));
this->btnReceiveText->SetColor(color::TextSecondary); this->btnReceiveText->SetColor(color::TextSecondary);
this->Add(this->btnReceiveText); this->Add(this->btnReceiveText);
this->panelFooter = pu::ui::elm::TextBlock::New( this->panelFooter = pu::ui::elm::TextBlock::New(PanelX + space::lg, ContentY + ContentH - space::lg - 18,
PanelX + space::lg,
ContentY + ContentH - space::lg - 18,
"Save data only"); "Save data only");
this->panelFooter->SetFont(type::font(type::Caption)); this->panelFooter->SetFont(type::font(type::Caption));
this->panelFooter->SetColor(color::TextMuted); this->panelFooter->SetColor(color::TextMuted);
this->Add(this->panelFooter); this->Add(this->panelFooter);
this->emptyText = pu::ui::elm::TextBlock::New( this->emptyText = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40,
ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40,
"No save data on this profile"); "No save data on this profile");
this->emptyText->SetFont(type::font(type::Display)); this->emptyText->SetFont(type::font(type::Display));
this->emptyText->SetColor(color::TextPrimary); this->emptyText->SetColor(color::TextPrimary);
this->emptyText->SetVisible(false); this->emptyText->SetVisible(false);
this->Add(this->emptyText); this->Add(this->emptyText);
this->emptySub = pu::ui::elm::TextBlock::New( this->emptySub = pu::ui::elm::TextBlock::New(ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16,
ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16,
"Play something first, then come back."); "Play something first, then come back.");
this->emptySub->SetFont(type::font(type::Body)); this->emptySub->SetFont(type::font(type::Body));
this->emptySub->SetColor(color::TextMuted); this->emptySub->SetColor(color::TextMuted);
@@ -145,7 +140,8 @@ namespace ui {
} }
void TitlesLayout::refreshPanel() { void TitlesLayout::refreshPanel() {
if (this->titlesMenu->GetItems().empty()) return; if (this->titlesMenu->GetItems().empty())
return;
int idx = this->titlesMenu->GetSelectedIndex(); int idx = this->titlesMenu->GetSelectedIndex();
Title title; Title title;
getTitle(title, this->current_uid, idx); getTitle(title, this->current_uid, idx);
@@ -219,7 +215,8 @@ namespace ui {
void TitlesLayout::runReceive(int index, Title& title) { void TitlesLayout::runReceive(int index, Title& title) {
if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) { if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) {
mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"},
true);
return; return;
} }
auto ovl = TransferOverlay::New("Receiving save data..."); auto ovl = TransferOverlay::New("Receiving save data...");
@@ -244,13 +241,17 @@ namespace ui {
} else if (mainApp->transfer.restoreSucceeded()) { } else if (mainApp->transfer.restoreSucceeded()) {
mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true);
} else { } else {
mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"}, true); mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"},
true);
} }
} }
void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
(void)Up; (void)Held; (void)Pos; (void)Up;
if (m_inputLocked) return; (void)Held;
(void)Pos;
if (m_inputLocked)
return;
if (Down & HidNpadButton_Plus) { if (Down & HidNpadButton_Plus) {
mainApp->transfer.cancelSend(); mainApp->transfer.cancelSend();
@@ -266,7 +267,8 @@ namespace ui {
return; return;
} }
if (Down & HidNpadButton_A) { if (Down & HidNpadButton_A) {
if (this->titlesMenu->GetItems().empty()) return; if (this->titlesMenu->GetItems().empty())
return;
this->lockedListIndex = this->titlesMenu->GetSelectedIndex(); this->lockedListIndex = this->titlesMenu->GetSelectedIndex();
this->focus = TitlesFocus::Actions; this->focus = TitlesFocus::Actions;
this->action = TitlesAction::Transfer; this->action = TitlesAction::Transfer;
@@ -284,10 +286,10 @@ namespace ui {
this->updateHints(); this->updateHints();
return; return;
} }
if (Down & (HidNpadButton_Up | HidNpadButton_Down | if (Down &
HidNpadButton_StickLUp | HidNpadButton_StickLDown)) { (HidNpadButton_Up | HidNpadButton_Down | HidNpadButton_StickLUp | HidNpadButton_StickLDown)) {
this->action = (action == TitlesAction::Transfer) this->action =
? TitlesAction::Receive : TitlesAction::Transfer; (action == TitlesAction::Transfer) ? TitlesAction::Receive : TitlesAction::Transfer;
this->refreshButtons(); this->refreshButtons();
return; return;
} }
@@ -307,4 +309,4 @@ namespace ui {
} }
} }
} }
} } // namespace ui
+6 -12
View File
@@ -6,11 +6,8 @@ namespace ui {
UsersLayout::UsersLayout() : Layout::Layout() { UsersLayout::UsersLayout() : Layout::Layout() {
using namespace theme; using namespace theme;
this->usersMenu = pu::ui::elm::Menu::New( this->usersMenu = pu::ui::elm::Menu::New(0, layout::ContentTop + space::md, layout::ScreenW,
0, layout::ContentTop + space::md, color::BgBase, color::BgSurface2, 88, 6);
layout::ScreenW,
color::BgBase, color::BgSurface2,
88, 6);
this->usersMenu->SetScrollbarColor(color::Primary); this->usersMenu->SetScrollbarColor(color::Primary);
this->usersMenu->SetItemsFocusColor(color::BgSurface2); this->usersMenu->SetItemsFocusColor(color::BgSurface2);
@@ -20,14 +17,11 @@ namespace ui {
this->usersMenu->AddItem(item); this->usersMenu->AddItem(item);
} }
this->loadingBg = pu::ui::elm::Rectangle::New( this->loadingBg = pu::ui::elm::Rectangle::New(0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
this->loadingBg->SetVisible(false); this->loadingBg->SetVisible(false);
this->loadingText = pu::ui::elm::TextBlock::New( this->loadingText =
layout::ScreenW / 2 - 120, pu::ui::elm::TextBlock::New(layout::ScreenW / 2 - 120, layout::ScreenH / 2 - 12, "Loading saves...");
layout::ScreenH / 2 - 12,
"Loading saves...");
this->loadingText->SetFont(type::font(type::Body)); this->loadingText->SetFont(type::font(type::Body));
this->loadingText->SetColor(color::TextSecondary); this->loadingText->SetColor(color::TextSecondary);
this->loadingText->SetVisible(false); this->loadingText->SetVisible(false);
@@ -72,4 +66,4 @@ namespace ui {
mainApp->LoadLayout(mainApp->titles_layout); mainApp->LoadLayout(mainApp->titles_layout);
} }
} }
} } // namespace ui