diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f188b96 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,95 @@ +name: Build + +on: + push: + branches: [main, packaging] + tags: ['*'] + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flet nrbf pytest pytest-asyncio + - name: Run tests + run: python -m pytest tests/ -v + + build-linux: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller flet nrbf + - name: Build Linux + run: | + pyinstaller --clean --onefile --name sailwind_editor_linux \ + --add-data "core.py:." \ + --add-data "ui.py:." \ + sailwind_editor.py + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: sailwind_editor_linux + path: dist/sailwind_editor_linux + + build-windows: + needs: test + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyinstaller flet nrbf + - name: Build Windows + shell: pwsh + run: | + pyinstaller --clean --onefile --name sailwind_editor_windows.exe ` + --add-data "core.py;." ` + --add-data "ui.py;." ` + sailwind_editor.py + - name: Upload Windows artifact + uses: actions/upload-artifact@v4 + with: + name: sailwind_editor_windows + path: dist/sailwind_editor_windows.exe + + release: + needs: [build-linux, build-windows] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: sailwind_editor_linux + path: dist/ + - uses: actions/download-artifact@v4 + with: + name: sailwind_editor_windows + path: dist/ + - name: Create release + uses: softprops/action-gh-release@v1 + with: + files: | + dist/sailwind_editor_linux + dist/sailwind_editor_windows.exe + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c364ef8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +*.so +.pytest_cache/ +.idea/ +.vscode/ +*.egg-info/ +dist/ +build/ +*.spec +*.log +*.bak +*.save +.env +.venv/ +venv/ +*.sqlite +*.db +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md index be09538..2d61e1e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Sailwind Save Editor -Cross-platform save file editor for the game **Sailwind** (Steam App ID 1764530). Built with Python and [Flet](https://flet.dev/) for native desktop UI (Windows, macOS, Linux) with future mobile/web support. +Cross-platform save file editor for the game **Sailwind** (Steam App ID 1764530). Built with Python and [Flet](https://flet.dev/) for native desktop UI (Windows, Linux). + +--- + +# Редактор сохранений Sailwind + +Кроссплатформенный редактор файлов сохранений для игры **Sailwind** (Steam App ID 1764530). Написан на Python с использованием [Flet](https://flet.dev/) для нативного интерфейса на Windows и Linux. ## Features @@ -9,32 +15,92 @@ Cross-platform save file editor for the game **Sailwind** (Steam App ID 1764530) - **Key fields highlighted** (playerGold, water/food/sleep, time/day, etc.) - **Auto-backup** creates `.bak` before saving - **Export/Import JSON** for manual editing or version control -- **Cross-platform** — Flet compiles to Windows/macOS/Linux desktop, with future Android/iOS/Web support +- **Cross-platform** — native desktop builds for Windows and Linux + +## Возможности + +- **Открытие `.save` файлов** через нативный файловый диалог +- **Редактирование всех полей** (Int32, Boolean, Int64, Single и др.) в табличном интерфейсе +- **Ключевые поля выделены** (playerGold, вода/еда/сон, время/день и др.) +- **Автобэкап** создаёт `.bak` перед сохранением +- **Экспорт/импорт JSON** для ручного редактирования или версионирования +- **Кроссплатформенность** — нативные сборки для Windows и Linux ## Installation +### From Releases (Recommended) + +Download the latest binary for your OS from the [Releases](https://github.com/qotique/SailwindSaveEditor/releases) page: + +- **Linux**: `sailwind_editor_linux` +- **Windows**: `sailwind_editor_windows.exe` + +Make executable and run (Linux): + +```bash +chmod +x sailwind_editor_linux +./sailwind_editor_linux ui +``` + +### From Source + ```bash pip install flet nrbf +python3 sailwind_editor.py ui +``` + +## Установка + +### Из релизов (Рекомендуется) + +Скачайте готовый бинарник для вашей ОС со страницы [Releases](https://github.com/qotique/SailwindSaveEditor/releases): + +- **Linux**: `sailwind_editor_linux` +- **Windows**: `sailwind_editor_windows.exe` + +Сделайте исполняемым и запустите (Linux): + +```bash +chmod +x sailwind_editor_linux +./sailwind_editor_linux ui ``` -Or use `pip install -e .` if packaging as a module. +### Из исходников + +```bash +pip install flet nrbf +python3 sailwind_editor.py ui +``` ## Usage -### CLI (original functionality preserved) +### GUI (default) + +```bash +# Linux +./sailwind_editor_linux + +# Windows +sailwind_editor_windows.exe +``` + +Opens native window with: +- **Open Save File** button +- Table of all editable fields with inline editing +- **Save File** (auto-backup + diff) +- **Export JSON** / **Import JSON** + +### CLI ```bash # Show editable fields -python3 sailwind_editor.py info [save_file] +./sailwind_editor_linux info [save_file] # Export to JSON for manual editing -python3 sailwind_editor.py dump [save_file] +./sailwind_editor_linux dump [save_file] # Import from JSON -python3 sailwind_editor.py pack [save_file] - -# Launch GUI -python3 sailwind_editor.py ui [save_file] +./sailwind_editor_linux pack [save_file] ``` Default save path (Steam Proton on Linux): @@ -42,17 +108,41 @@ Default save path (Steam Proton on Linux): ~/.local/share/Steam/steamapps/compatdata/1764530/pfx/drive_c/users/steamuser/AppData/LocalLow/Raw Lion Workshop/Sailwind/slot0.save ``` -### GUI +## Использование + +### GUI (по умолчанию) ```bash -python3 sailwind_editor.py ui +# Linux +./sailwind_editor_linux + +# Windows +sailwind_editor_windows.exe ``` -Opens a native window with: -- **Open Save File** button -- Table of all editable fields with inline editing -- **Save File** (auto-backup + diff) -- **Export JSON** / **Import JSON** +Открывается нативное окно с: +- Кнопка открытия файла сохранения +- Таблица всех редактируемых полей с inline-редактированием +- **Сохранение файла** (автобэкап + дифф) +- **Экспорт/импорт JSON** + +### CLI + +```bash +# Показать редактируемые поля +./sailwind_editor_linux info [файл_сохранения] + +# Экспорт в JSON для ручного редактирования +./sailwind_editor_linux dump [файл_сохранения] + +# Импорт из JSON +./sailwind_editor_linux pack [файл_сохранения] +``` + +Путь к сохранению по умолчанию (Steam Proton на Linux): +``` +~/.local/share/Steam/steamapps/compatdata/1764530/pfx/drive_c/users/steamuser/AppData/LocalLow/Raw Lion Workshop/Sailwind/slot0.save +``` ## Project Structure @@ -67,12 +157,31 @@ sailwind_editor/ └── README.md ``` +## Структура проекта + +``` +sailwind_editor/ +├── core.py # Ядро: бинарный патчинг (класс SailwindSave) +├── ui.py # GUI на Flet +├── sailwind_editor.py # CLI точка входа (dump/pack/info/ui) +├── tests/ +│ ├── conftest.py # Фикстуры тестов (генератор мок-сохранений) +│ └── test_core.py # Юнит-тесты (15 тестов проходят) +└── README.md +``` + ## Architecture - **core.py** — Pure Python, no GUI dependencies. Parses .NET BinaryFormatter (NRBF) save files, finds field offsets, patches binary data in-place. -- **ui.py** — Flet-based GUI. Uses `FilePicker` for file selection, `TextField` for inline editing. Runs on desktop; future-proof for mobile/web. +- **ui.py** — Flet-based GUI. Uses `FilePicker` for file selection, `TextField` for inline editing. Runs on desktop. - **sailwind_editor.py** — Thin CLI wrapper reusing core logic. +## Архитектура + +- **core.py** — Чистый Python, без GUI-зависимостей. Парсит .NET BinaryFormatter (NRBF), находит смещения полей, патчит бинарные данные inplace. +- **ui.py** — GUI на Flet. Использует `FilePicker` для выбора файла, `TextField` для inline-редактирования. Работает на десктопе. +- **sailwind_editor.py** — Тонкая CLI-обёртка, переиспользующая логику core. + ## Testing ```bash @@ -87,11 +196,34 @@ All 15 tests pass, covering: - Backup creation - Error handling +## Тестирование + +```bash +cd /home/u/sailwind_editor +python3 -m pytest tests/ -v +``` + +Все 15 тестов проходят, покрывают: +- Обнаружение полей (скалярные/массивы, Int32/Boolean) +- Операции патчинга (скалярные/массивы, граничные случаи Boolean) +- JSON экспорт/импорт туда-обратно +- Создание бэкапов +- Обработку ошибок + ## License MIT License — see [LICENSE](LICENSE). +## Лицензия + +MIT License — см. [LICENSE](LICENSE). + ## Credits - NRBF parsing via [`nrbf`](https://pypi.org/project/nrbf/) by @vickas -- UI via [Flet](https://flet.dev/) (Flutter-powered Python framework) \ No newline at end of file +- UI via [Flet](https://flet.dev/) (Flutter-powered Python framework) + +## Благодарности + +- Парсинг NRBF через [`nrbf`](https://pypi.org/project/nrbf/) от @vickas +- UI через [Flet](https://flet.dev/) (Flutter-powered Python framework) diff --git a/sailwind_editor.py b/sailwind_editor.py index b41a18c..8d4d628 100644 --- a/sailwind_editor.py +++ b/sailwind_editor.py @@ -55,22 +55,25 @@ def cmd_info(save_path): def main(): args = sys.argv[1:] - cmd = args[0] if args else 'info' - save = args[1] if len(args) > 1 else DEFAULT_SAVE - - if not os.path.exists(save): - print(f"Save file not found: {save}") - sys.exit(1) - - if cmd == 'dump': - cmd_dump(save) - elif cmd == 'pack': - cmd_pack(save) - elif cmd == 'info': - cmd_info(save) + if not args: + from ui import main as ui_main + ui_main() + return + cmd = args[0] + if cmd in ('dump', 'pack', 'info'): + save = args[1] if len(args) > 1 else DEFAULT_SAVE + if not os.path.exists(save): + print(f"Save file not found: {save}") + sys.exit(1) + if cmd == 'dump': + cmd_dump(save) + elif cmd == 'pack': + cmd_pack(save) + elif cmd == 'info': + cmd_info(save) elif cmd == 'ui': - from ui import main - main() + from ui import main as ui_main + ui_main() else: print(__doc__) diff --git a/sailwind_editor.spec b/sailwind_editor.spec new file mode 100644 index 0000000..03aaf85 --- /dev/null +++ b/sailwind_editor.spec @@ -0,0 +1,115 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +a = Analysis( + ['sailwind_editor.py'], + pathex=[], + binaries=[], + datas=[ + ('core.py', '.'), + ('ui.py', '.'), + ], + hiddenimports=[ + 'flet', + 'flet.core', + 'flet.controls', + 'flet.controls.text', + 'flet.controls.textfield', + 'flet.controls.button', + 'flet.controls.filepicker', + 'flet.controls.progressbar', + 'flet.controls.text', + 'flet.controls.button', + 'flet.controls.checkbox', + 'flet.controls.dropdown', + 'flet.controls.row', + 'flet.controls.column', + 'flet.controls.container', + 'flet.controls.appbar', + 'flet.controls.icon', + 'flet.controls.divider', + 'flet.controls.tabs', + 'flet.controls.tab', + 'flet.controls.card', + 'flet.controls.listview', + 'flet.controls.gridview', + 'flet.controls.alertdialog', + 'flet.controls.textfield', + 'flet.controls.filledbutton', + 'flet.controls.outlinedbutton', + 'flet.controls.iconbutton', + 'flet.controls.textbutton', + 'flet.controls.elevatedbutton', + 'flet.controls.floatingactionbutton', + 'flet.controls.tooltip', + 'flet.controls.gesturedetector', + 'flet.controls.stack', + 'flet.controls.shader_mask', + 'flet.controls.opacity', + 'flet.controls.rotation', + 'flet.controls.scale', + 'flet.controls.transform', + 'flet.controls.animate', + 'flet.controls.animation', + 'flet.core.control', + 'flet.core.control_event', + 'flet.core.event', + 'flet.core.animation', + 'flet.core.animation_curve', + 'flet.core.border', + 'flet.core.border_radius', + 'flet.core.padding', + 'flet.core.margin', + 'flet.core.alignment', + 'flet.core.colors', + 'flet.core.fonts', + 'flet.core.icons', + 'flet.core.theme', + 'flet.core.theme_mode', + 'flet.core.control_event', + 'flet.core.file_picker_result', + 'flet.core.event_handler', + 'flet.core.protocol', + 'flet.core.protocol_handler', + 'flet_runtime', + 'flet_runtime.python', + 'nrbf', + 'nrbf.nrbf', + 'nrbf.parser', + 'nrbf.serializer', + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='sailwind_editor', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=None, +) \ No newline at end of file