From e86eca4d1efd99c4034574225ef312f5236de89e Mon Sep 17 00:00:00 2001 From: Nikolai Fedorov Date: Fri, 24 Apr 2026 09:44:15 +0300 Subject: [PATCH] first working prototype --- CLAUDE.md | 65 +++++++++++++++++++++++++++ Makefile | 2 +- source/TitlesLayout.cpp | 2 +- source/account.cpp | 2 +- source/io.cpp | 49 ++++++++++++++++++--- source/server.cpp | 98 ++++++++++++++++++++++++++++++++++------- 6 files changed, 191 insertions(+), 27 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..daced9b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. + +**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. + +## 1. Think Before Coding + +**Don't assume. Don't hide confusion. Surface tradeoffs.** + +Before implementing: +- State your assumptions explicitly. If uncertain, ask. +- If multiple interpretations exist, present them - don't pick silently. +- If a simpler approach exists, say so. Push back when warranted. +- If something is unclear, stop. Name what's confusing. Ask. + +## 2. Simplicity First + +**Minimum code that solves the problem. Nothing speculative.** + +- No features beyond what was asked. +- No abstractions for single-use code. +- No "flexibility" or "configurability" that wasn't requested. +- No error handling for impossible scenarios. +- If you write 200 lines and it could be 50, rewrite it. + +Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. + +## 3. Surgical Changes + +**Touch only what you must. Clean up only your own mess.** + +When editing existing code: +- Don't "improve" adjacent code, comments, or formatting. +- Don't refactor things that aren't broken. +- Match existing style, even if you'd do it differently. +- If you notice unrelated dead code, mention it - don't delete it. + +When your changes create orphans: +- Remove imports/variables/functions that YOUR changes made unused. +- Don't remove pre-existing dead code unless asked. + +The test: Every changed line should trace directly to the user's request. + +## 4. Goal-Driven Execution + +**Define success criteria. Loop until verified.** + +Transform tasks into verifiable goals: +- "Add validation" → "Write tests for invalid inputs, then make them pass" +- "Fix the bug" → "Write a test that reproduces it, then make it pass" +- "Refactor X" → "Ensure tests pass before and after" + +For multi-step tasks, state a brief plan: +``` +1. [Step] → verify: [check] +2. [Step] → verify: [check] +3. [Step] → verify: [check] +``` + +Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. + +--- + +**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. diff --git a/Makefile b/Makefile index b2a96f1..730fabb 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ INCLUDES := include include/fs lib/Plutonium/include EXEFS_SRC := exefs_src APP_TITLE := NXST APP_AUTHOR := DragonSpirit -APP_VERSION := 01.28.2024 +APP_VERSION := 04.24.2026 ICON := icon.jpg diff --git a/source/TitlesLayout.cpp b/source/TitlesLayout.cpp index 613d28b..26a2ed7 100644 --- a/source/TitlesLayout.cpp +++ b/source/TitlesLayout.cpp @@ -63,7 +63,7 @@ namespace ui { printf("Failed to restore with error %s\n", std::get<2>(restoreResult).c_str()); } } - + break; } } diff --git a/source/account.cpp b/source/account.cpp index 37dc212..1bca46f 100644 --- a/source/account.cpp +++ b/source/account.cpp @@ -56,7 +56,7 @@ static User getUser(AccountUid id) memset(&profilebase, 0, sizeof(profilebase)); if (R_SUCCEEDED(accountGetProfile(&profile, id)) && R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) { - user.name = std::string(profilebase.nickname, 0x20); + user.name = std::string(profilebase.nickname); } accountProfileClose(&profile); diff --git a/source/io.cpp b/source/io.cpp index 360b78f..ca00b14 100644 --- a/source/io.cpp +++ b/source/io.cpp @@ -70,9 +70,7 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath) fclose(src); fclose(dst); - // commit each file to the save if (dstPath.rfind("save:/", 0) == 0) { - Logger::getInstance().log(Logger::ERROR, "Committing file " + dstPath + " to the save archive."); fsdevCommitDevice("save"); } @@ -217,34 +215,71 @@ std::tuple io::restore(size_t index, AccountUid uid, Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(), title.userId().uid[1], title.userId().uid[0]); + // Если сейв ещё не существует (игра не запускалась) — создаём его через NACP. + // fsCreateSaveDataFileSystem возвращает ошибку если сейв уже есть — это нормально. + { + NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData)); + if (nsacd != NULL) { + memset(nsacd, 0, sizeof(NsApplicationControlData)); + size_t outsize = 0; + if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) { + static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail}; + + FsSaveDataAttribute attr = {}; + attr.application_id = title.id(); + attr.uid = uid; + attr.save_data_type = FsSaveDataType_Account; + attr.save_data_rank = FsSaveDataRank_Primary; + + FsSaveDataCreationInfo createInfo = {}; + createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size; + createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size; + createInfo.available_size = 0x4000; + createInfo.owner_id = nsacd->nacp.save_data_owner_id; + createInfo.save_data_space_id = FsSaveDataSpaceId_User; + + fsCreateSaveDataFileSystem(&attr, &createInfo, &meta); + } + free(nsacd); + } + } + FsFileSystem fileSystem; - res = FileSystem::mount(&fileSystem, title.id(), title.userId()); + res = FileSystem::mount(&fileSystem, title.id(), uid); if (R_SUCCEEDED(res)) { int rc = FileSystem::mount(fileSystem); if (rc == -1) { FileSystem::unmount(); Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(), - title.userId().uid[1], title.userId().uid[0]); + uid.uid[1], uid.uid[0]); return std::make_tuple(false, -2, "Failed to mount save."); } } else { Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(), - title.userId().uid[1], title.userId().uid[0]); + uid.uid[1], uid.uid[0]); return std::make_tuple(false, res, "Failed to mount save."); } - std::string srcPath = title.fullPath(cellIndex) + "/"; + std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid))); + std::string srcPath = title.path() + "/" + suggestion + "/"; std::string dstPath = "save:/"; res = io::deleteFolderRecursively(dstPath.c_str()); if (R_FAILED(res)) { FileSystem::unmount(); - Logger::getInstance().log(Logger::ERROR, "Failed to recursively delete directory " + dstPath + " with result 0x%08lX.", res); + Logger::getInstance().log(Logger::ERROR, "Failed to recursively delete directory %s with result 0x%08lX.", dstPath.c_str(), res); return std::make_tuple(false, res, "Failed to delete save."); } + res = fsdevCommitDevice("save"); + if (R_FAILED(res)) { + FileSystem::unmount(); + Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res); + return std::make_tuple(false, res, "Failed to commit save after delete."); + } + res = io::copyDirectory(srcPath, dstPath); if (R_FAILED(res)) { FileSystem::unmount(); diff --git a/source/server.cpp b/source/server.cpp index 5873062..ad2f259 100644 --- a/source/server.cpp +++ b/source/server.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include @@ -27,7 +29,8 @@ namespace fs = std::filesystem; #ifdef __SWITCH__ std::string replaceUsername(const std::string &path) { - std::string replacedString = Account::username(g_currentUId); + std::string replacedString = StringUtils::removeNotAscii( + StringUtils::removeAccents(Account::username(g_currentUId))); // Найдём позицию последнего символа '/' size_t lastSlashPos = path.rfind('/'); @@ -57,19 +60,75 @@ void sendAck(int sock) { send(sock, ack, strlen(ack), 0); } +// Читает ровно len байт из сокета, повторяя read при частичном получении. +static bool recv_all(int sock, void *buf, size_t len) { + size_t received = 0; + while (received < len) { + ssize_t n = read(sock, static_cast(buf) + received, len - received); + if (n <= 0) return false; + received += n; + } + return true; +} + +// Создаёт все компоненты пути через POSIX mkdir. +// std::filesystem::create_directories не работает с devkitPro-путями sdmc:/. +static void mkdirs(const std::string &path) { + for (size_t i = 1; i < path.size(); i++) { + if (path[i] == '/') { + std::string component = path.substr(0, i); + int rc = mkdir(component.c_str(), 0777); + std::cout << "mkdirs: mkdir [" << component << "] rc=" << rc << " errno=" << errno << std::endl; + } + } + int rc = mkdir(path.c_str(), 0777); + std::cout << "mkdirs: mkdir [" << path << "] rc=" << rc << " errno=" << errno << std::endl; +} + // Функция для получения файла void receive_file(int sock, const std::string &relative_path, size_t file_size) { - fs::path filepath(relative_path); - // Create parent directories if they do not exist - std::cout << "relative_path is: " << relative_path << std::endl; - fs::create_directories(filepath.parent_path()); + // Печатаем путь побайтово — ловим невидимые символы + std::cout << "receive_file len=" << relative_path.size() << " path=["; + for (unsigned char c : relative_path) { + if (c >= 0x20 && c <= 0x7e) std::cout << c; + else std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0') << (int)c << std::dec; + } + std::cout << "]" << std::endl; + + size_t last_slash = relative_path.rfind('/'); + std::string dir = (last_slash != std::string::npos) + ? relative_path.substr(0, last_slash) + : ""; + std::cout << "receive_file dir=[" << dir << "]" << std::endl; + if (!dir.empty()) mkdirs(dir); + + // Проверяем stat на папке перед fopen + struct stat st; + int statrc = stat(dir.c_str(), &st); + std::cout << "stat(dir) rc=" << statrc << " is_dir=" + << (statrc == 0 && S_ISDIR(st.st_mode)) << std::endl; + + FILE *outfile = fopen(relative_path.c_str(), "wb"); + if (!outfile) { + int saved_errno = errno; + std::cerr << "Failed to open for writing: " << relative_path + << " dir=[" << dir << "] fopen_errno=" << saved_errno << std::endl; + // Дренируем байты, чтобы отправитель не завис + char buf[BUFFER_SIZE]; + size_t remaining = file_size; + while (remaining > 0) { + ssize_t n = read(sock, buf, remaining < BUFFER_SIZE ? remaining : BUFFER_SIZE); + if (n <= 0) break; + remaining -= n; + sendAck(sock); + } + return; + } - std::ofstream outfile(filepath, std::ios::binary); char buffer[BUFFER_SIZE] = {0}; - size_t total_bytes_received = 0; while (total_bytes_received < file_size) { ssize_t bytes_received = read(sock, buffer, BUFFER_SIZE); @@ -78,7 +137,7 @@ void receive_file(int sock, const std::string &relative_path, std::cerr << "Error reading file data from socket." << std::endl; break; } - outfile.write(buffer, bytes_received); + fwrite(buffer, 1, bytes_received, outfile); total_bytes_received += bytes_received; // Send acknowledgment for each chunk received @@ -86,7 +145,7 @@ void receive_file(int sock, const std::string &relative_path, } std::cout << "File received successfully: " << relative_path << std::endl; - outfile.close(); + fclose(outfile); } void *handle_client(void *socket_desc) { @@ -108,14 +167,16 @@ void *handle_client(void *socket_desc) { pthread_exit(nullptr); } - // Receive filename or directory name - char *filename = new char[filename_len + 1]; - read(client_socket, filename, filename_len); - + // Receive filename + char *filename = new char[filename_len + 1](); + if (!recv_all(client_socket, filename, filename_len)) { + std::cerr << "Short read on filename, aborting." << std::endl; + delete[] filename; + break; + } filename[filename_len] = '\0'; std::string filename_str(filename); - std::cout << "Receive filename: " << filename_str << std::endl; - delete[] filename; // Clean up filename buffer + delete[] filename; std::cout << "Received filename_str is " << filename_str << std::endl; @@ -126,7 +187,10 @@ void *handle_client(void *socket_desc) { #endif size_t file_size; - read(client_socket, &file_size, sizeof(file_size)); + if (!recv_all(client_socket, &file_size, sizeof(file_size))) { + std::cerr << "Short read on file_size, aborting." << std::endl; + break; + } std::cout << "file size is: " << file_size << std::endl; receive_file(client_socket, filename_str, file_size); } @@ -138,7 +202,7 @@ void *handle_client(void *socket_desc) { void *broadcast_listener(void *) { int sockfd; struct sockaddr_in servaddr; - char buffer[BUFFER_SIZE]; + char buffer[BUFFER_SIZE + 1]; struct ip_mreq group; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {