From 0e74aaa7eee3ed3e59a6f27615ddf25cac5b68f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=9F=D0=B5=D1=82=D1=80=D0=BE=D0=B2?= Date: Tue, 15 Sep 2026 23:04:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BD=D0=BE?= =?UTF-8?q?=D0=BC=D0=BD=D1=83=D1=8E=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D1=83?= =?UTF-8?q?=20=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=D0=BC=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 54 +- README.md | 23 +- app.mk | 70 +- applications/PoP/.gitignore | 29 + applications/PoP/.python-version | 1 + applications/PoP/LICENSE | 21 + applications/PoP/bgtest/Makefile | 9 +- applications/PoP/coltest/Makefile | 3 +- applications/PoP/poc/Makefile | 5 +- .../PoP/poc/tools/build_room_palette.py | 3 +- .../PoP/poc/tools/build_tile_atlases.py | 3 +- .../PoP/poc/tools/gen_kid_placeholder.py | 2 +- applications/PoP/roomtest/Makefile | 17 +- applications/PoP/roomtest/tests-host/Makefile | 8 +- applications/PoP/sdk.mk | 11 + .../third_party/16x16-RPG-characters/LICENSE | 359 +++++ .../16x16-RPG-characters/README.md | 27 + .../sprites/old-style/02-bard.png | Bin 0 -> 7830 bytes .../PoP/toolchain/pop_extract_font.py | 208 +++ applications/PoP/toolchain/pop_pack_intro.py | 349 +++++ applications/PoP/toolchain/pop_pack_kid.py | 2 +- applications/PoP/toolchain/pop_pack_title.py | 137 ++ applications/PoP/toolchain/render_room.py | 7 +- applications/SprPoP/.gitignore | 4 + applications/SprPoP/LICENSE | 21 + applications/SprPoP/Makefile | 23 +- applications/SprPoP/README.md | 5 +- applications/SprPoP/tests/host/Makefile | 6 +- applications/Volkov/.gitignore | 15 + applications/Volkov/.python-version | 1 + applications/Volkov/LICENSE | 21 + applications/Volkov/Makefile | 5 +- applications/Volkov/sdk.mk | 11 + .../Volkov/tests/fixtures/mdview2/LICENSE | 41 + .../Volkov/tests/fixtures/mdview2/ORIGIN.md | 8 + .../Volkov/tests/fixtures/mdview2/README.md | 153 ++ .../Volkov/tests/fixtures/mdview2/mdview2.c | 1338 +++++++++++++++++ .../Volkov/tests/fixtures/mdview2/mdview2.h | 304 ++++ .../tests/fixtures/mdview2/mdview2_conf.h | 16 + .../tests/fixtures/mdview2/mdview2_enc.c | 319 ++++ .../tests/fixtures/mdview2/mdview2_help.c | 106 ++ .../tests/fixtures/mdview2/mdview2_hex.c | 209 +++ .../tests/fixtures/mdview2/mdview2_index.c | 1031 +++++++++++++ .../tests/fixtures/mdview2/mdview2_md.c | 227 +++ .../tests/fixtures/mdview2/mdview2_raw.c | 308 ++++ .../tests/fixtures/mdview2/mdview2_status.c | 192 +++ .../tests/fixtures/mdview2/mdview2_table.c | 91 ++ .../Volkov/tests/make_p10_rename_hdd.sh | 3 +- .../Volkov/tests/make_p11_delete_hdd.sh | 3 +- .../Volkov/tests/make_p12_group_copy_hdd.sh | 3 +- .../Volkov/tests/make_p13_group_delete_hdd.sh | 3 +- .../tests/make_p14_default_colors_hdd.sh | 3 +- .../Volkov/tests/make_p15_tree_cancel_hdd.sh | 3 +- .../Volkov/tests/make_p15_tree_copy_hdd.sh | 3 +- .../Volkov/tests/make_p15_tree_self_hdd.sh | 3 +- .../tests/make_p16_group_tree_copy_hdd.sh | 3 +- .../tests/make_p17_tree_delete_cancel_hdd.sh | 3 +- .../Volkov/tests/make_p17_tree_delete_hdd.sh | 3 +- .../Volkov/tests/make_p18_metadata_hdd.sh | 3 +- .../Volkov/tests/make_p19_overwrite_hdd.sh | 3 +- .../Volkov/tests/make_p19_policy_hdd.sh | 3 +- .../Volkov/tests/make_p3_stress_floppy.sh | 5 +- .../Volkov/tests/make_p3_stress_hdd.sh | 3 +- applications/Volkov/tests/make_p4_copy_hdd.sh | 3 +- applications/Volkov/tests/make_p4_exec_hdd.sh | 3 +- .../Volkov/tests/make_p5_copy_faults_hdd.sh | 3 +- .../Volkov/tests/make_p5_enospc_hdd.sh | 3 +- applications/Volkov/tests/make_p5_hdd.sh | 3 +- .../Volkov/tests/make_p5_media_hdd.sh | 3 +- .../Volkov/tests/make_p5_panel_edges_hdd.sh | 3 +- .../Volkov/tests/make_p5_viewer_hdd.sh | 7 +- applications/Volkov/tests/make_p6_sort_hdd.sh | 3 +- .../Volkov/tests/make_p7_select_hdd.sh | 3 +- .../Volkov/tests/make_p9_mkdir_hdd.sh | 3 +- .../Volkov/tests/p1_platform/Makefile | 3 +- applications/Volkov/tests/p2_palette/Makefile | 3 +- applications/Volkov/tests/p2_screen/Makefile | 3 +- .../Volkov/tests/p3_dir_probe/Makefile | 3 +- .../Volkov/tests/p3_enum_matrix/Makefile | 3 +- .../Volkov/tests/p3_scan_probe/Makefile | 3 +- .../Volkov/tests/p3_store_probe/Makefile | 3 +- .../Volkov/tests/p4_bank_io_probe/Makefile | 3 +- .../Volkov/tests/p4_exec_child/Makefile | 3 +- applications/Volkov/tests/run_mame_hdd.sh | 41 +- docs/project-reorganization.md | 10 +- examples/.gitignore | 22 + examples/.python-version | 1 + examples/LICENSE | 21 + examples/Makefile | 11 + examples/README.md | 22 + examples/balls/Makefile | 3 +- examples/mdview/Makefile | 7 +- examples/mdview2/Makefile | 7 +- examples/rpgwalk/Makefile | 3 +- examples/rpgwalk/conv_sprites.py | 2 +- examples/scroll/Makefile | 3 +- examples/sdk.mk | 8 + examples/space/Makefile | 3 +- .../third_party/16x16-RPG-characters/LICENSE | 359 +++++ .../16x16-RPG-characters/README.md | 27 + .../sprites/old-style/02-bard.png | Bin 0 -> 7830 bytes tests/sdbg/run_mame_probe.py | 29 +- tests/w3probe/Makefile | 5 +- toolchain/make_disk.py | 242 +++ toolchain/make_hdd.sh | 15 +- toolchain/make_release.sh | 24 +- toolchain/mame_interactive.py | 135 +- toolchain/mame_profile.py | 85 ++ toolchain/run_sprinter_mame.py | 77 + toolchain/sdbg_dap.py | 11 + toolchain/sdbg_launcher.py | 56 +- toolchain/vscode-sprinter-debug/LICENSE | 21 + toolchain/vscode-sprinter-debug/build.js | 2 +- toolchain/vscode-sprinter-debug/extension.js | 18 +- toolchain/vscode-sprinter-debug/package.json | 39 +- toolchain/vscode-sprinter-debug/runtime.js | 15 +- 116 files changed, 6908 insertions(+), 333 deletions(-) create mode 100644 applications/PoP/.gitignore create mode 100644 applications/PoP/.python-version create mode 100644 applications/PoP/LICENSE create mode 100644 applications/PoP/sdk.mk create mode 100644 applications/PoP/third_party/16x16-RPG-characters/LICENSE create mode 100644 applications/PoP/third_party/16x16-RPG-characters/README.md create mode 100644 applications/PoP/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png create mode 100644 applications/PoP/toolchain/pop_extract_font.py create mode 100644 applications/PoP/toolchain/pop_pack_intro.py create mode 100644 applications/PoP/toolchain/pop_pack_title.py create mode 100644 applications/SprPoP/LICENSE create mode 100644 applications/Volkov/.python-version create mode 100644 applications/Volkov/LICENSE create mode 100644 applications/Volkov/sdk.mk create mode 100644 applications/Volkov/tests/fixtures/mdview2/LICENSE create mode 100644 applications/Volkov/tests/fixtures/mdview2/ORIGIN.md create mode 100644 applications/Volkov/tests/fixtures/mdview2/README.md create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2.h create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_conf.h create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_enc.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_help.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_hex.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_index.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_md.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_raw.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_status.c create mode 100644 applications/Volkov/tests/fixtures/mdview2/mdview2_table.c create mode 100644 examples/.gitignore create mode 100644 examples/.python-version create mode 100644 examples/LICENSE create mode 100644 examples/Makefile create mode 100644 examples/README.md create mode 100644 examples/sdk.mk create mode 100644 examples/third_party/16x16-RPG-characters/LICENSE create mode 100644 examples/third_party/16x16-RPG-characters/README.md create mode 100644 examples/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png create mode 100755 toolchain/make_disk.py create mode 100644 toolchain/mame_profile.py create mode 100644 toolchain/run_sprinter_mame.py create mode 100644 toolchain/vscode-sprinter-debug/LICENSE diff --git a/Makefile b/Makefile index 680e29d..9b76c42 100644 --- a/Makefile +++ b/Makefile @@ -4,12 +4,7 @@ # make tools build only host tools (mkexe) # make lib build lib/sprinter.lib (libc) + lib/bgi256.lib (libbgi) # make tests build all libc feature tests under tests/ -# make examples build all real applications under examples/ (НЕ входит -# в `make all`: это регрессная сборка, а examples/ — -# крупные приложения, которые её только замедляют -# (mdview компилируется минутами) и ничего нового про -# libc не показывают. Собирать явно перед `make floppy`.) -# make floppy package every .exe + test fixtures into mame/v306/IMG/mc.img +# make floppy пакет всех SDK-тестов в build/media/toolkit-tests.img # make check run mkexe unit tests # make clean remove all build artefacts # make sdcc download/extract vendored SDCC @@ -25,25 +20,20 @@ TESTS := hello hello2 simple banked bankedbg banktest strtest cat seek \ gfx_demo gfx_dbuf bgitest bgi_img accfill # gfx_d16 / gfx_text / gfx_mous — 16-цветные; убраны до Фазы 2 (bgi16.lib # ещё не собирается). Вернуть мигрированными на BGI --gfx 16. -# Larger end-user applications under examples/. -APPS := mdview mdview2 - -MAME_DIR := mame/v306 -FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img -MAKE_DISK := $(MAME_DIR)/make_disk.py +FLOPPY_IMG := build/media/toolkit-tests.img +MAKE_DISK := toolchain/make_disk.py +MAME_HOME ?= $(if $(wildcard mame/v306/mame.arm),$(CURDIR)/mame/v306,) TEST_EXES := $(foreach t,$(TESTS),tests/$(t)/$(t).exe) -APP_EXES := $(foreach a,$(APPS),examples/$(a)/$(a).exe) -ALL_EXES := $(TEST_EXES) $(APP_EXES) +ALL_EXES := $(TEST_EXES) DATA_FILES := \ tests/cat/test.txt \ tests/seek/big.txt \ - tests/cblwav/speech.pcm \ - examples/mdview/SAMPLE.MD + tests/cblwav/speech.pcm -.PHONY: all tools lib tests examples check clean sdcc floppy \ - size-check size-baseline host-tests sdbg-tests mame-sdbg-patch mame-sdbg $(TESTS) $(APPS) +.PHONY: all tools lib tests check clean sdcc floppy run \ + size-check size-baseline host-tests sdbg-tests $(TESTS) all: tools lib tests @@ -58,31 +48,30 @@ check: tools $(MAKE) -C toolchain/mkexe check tests: $(TESTS) -examples: $(APPS) $(TESTS): tools lib $(MAKE) -C tests/$@ -$(APPS): tools lib - $(MAKE) -C examples/$@ # Generate big.txt if missing (gen_bigfile.py creates 100 KB marker file). tests/seek/big.txt: cd tests/seek && python3 gen_bigfile.py big.txt 102400 -# Re-pack the MAME floppy image with every built exe + needed data files. -floppy: tests examples tests/seek/big.txt +# Собственный SDK-носитель: не изменяет диск MAME или приложения. +floppy: tests tests/seek/big.txt python3 $(MAKE_DISK) $(FLOPPY_IMG) $(ALL_EXES) $(DATA_FILES) @echo @echo "Floppy ready: $(FLOPPY_IMG)" - @echo "Run: cd $(MAME_DIR) && ./run_mame.sh" + @echo "Run: make run MAME_HOME=/путь/к/MAME/runtime" + +run: floppy + python3 toolchain/run_sprinter_mame.py --mame-home "$(MAME_HOME)" --floppy "$(FLOPPY_IMG)" # Модульные тесты под ucsim_z80. Обвязка — testkit/, сами наборы лежат # рядом с кодом, который проверяют. MAME не нужна, идут за секунды; # ucsim идёт в комплекте нашего SDCC. -# applications/PoP/roomtest заморожена (её ветка развития — SprPoP), поэтому -# её набор здесь больше не гоняется. -HOST_TEST_DIRS := testkit applications/SprPoP/tests/host +# Тесты продуктов запускаются в собственных репозиториях. +HOST_TEST_DIRS := testkit host-tests: @for d in $(HOST_TEST_DIRS); do $(MAKE) -C $$d || exit 1; done @@ -92,16 +81,6 @@ host-tests: sdbg-tests: tools lib python3 -m unittest discover -s tests/sdbg -v -# Backend удерживает MAME в stopped-loop без Cocoa debugger; Lua bridge при -# этом обслуживается из periodic_check ядра. Повторный вызов безопасен. -mame-sdbg-patch: - sh toolchain/apply-mame-sdbg-patch.sh - -# Инкрементально собирает patched checkout и устанавливает бинарник в v306. -mame-sdbg: mame-sdbg-patch - $(MAKE) -C mame/sources/MAME - cp mame/sources/MAME/mame $(MAME_DIR)/mame.arm - # Размерный регресс: сверить _CODE всех программ с docs/size_baseline.tsv. size-check: python3 toolchain/size_check.py @@ -115,7 +94,6 @@ clean: $(MAKE) -C libc clean $(MAKE) -C libbgi clean @for t in $(TESTS); do $(MAKE) -C tests/$$t clean; done - @for a in $(APPS); do $(MAKE) -C examples/$$a clean; done sdcc: bash third_party/setup-sdcc.sh diff --git a/README.md b/README.md index 76dafa0..779ecf8 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and git clone sprinter-c cd sprinter-c make sdcc # one-time: fetch SDCC 4.5 binary (~25 MB) -make all # build mkexe + libsprinter.lib + 27 examples -make floppy # pack everything into mame/v306/IMG/mc.img -cd mame/v306 && ./run_mame.sh # boot Sprinter in MAME +make all # build tools, libraries and SDK tests +make floppy # pack tests into build/media/toolkit-tests.img +MAME_HOME=/путь/к/MAME/runtime make run ``` Compile a single program: @@ -70,7 +70,11 @@ Banked functions are declared with `__banked`: void engine_tick(int dt) __banked; // lives in BANK1, automatically swapped ``` -## Examples (27 total) +## SDK tests and separate examples + +Programs listed below live in `tests/` and validate the SDK. Demonstration +applications live in the separate `Examples` repository. Its Makefiles use +`SPRINTER_ROOT` to find this SDK. | Example | What it demonstrates | |---|---| @@ -128,8 +132,9 @@ Sprinter-specific: ## Toolchain commands ```sh -make all # build mkexe + lib + every example -make floppy # repack mame/v306/IMG/mc.img with all .exe files +make all # build mkexe + libraries + SDK tests +make floppy # pack SDK tests into build/media/toolkit-tests.img +MAME_HOME=/путь/к/MAME/runtime make run make check # 17 mkexe unit-tests make clean # remove all build artefacts make sdcc # one-time: fetch SDCC 4.5 binary @@ -198,8 +203,8 @@ runtime/ crt0 variants (default, minimal, small, banked) libc/include/ headers libc/io|stdio|mem|gfx/ C and asm sources for libsprinter.lib lib/ Makefile that archives libsprinter.lib via sdar -examples/ 27 example programs -mame/v306/ MAME binary + Sprinter ROM/HDD images + floppy script +tests/ SDK regression programs +app.mk shared rules for independent applications third_party/sdcc/ vendored SDCC 4.5 (fetched via `make sdcc`) third_party/solid-c/ reference: original Sprinter native C (for compat target) docs/ documentation @@ -208,7 +213,7 @@ docs/ documentation ## License This repository contains: -* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `lib/`, `examples/` — +* Original code in `bin/`, `toolchain/`, `runtime/`, `libc/`, `libbgi/`, `lib/` — MIT-licensed. * `third_party/sdcc/` — SDCC 4.5 under GPLv2 with linking exception (see `third_party/sdcc/COPYING.txt`) diff --git a/app.mk b/app.mk index 4269feb..6cadeae 100644 --- a/app.mk +++ b/app.mk @@ -1,10 +1,9 @@ # app.mk — shared Makefile fragment for any standalone Sprinter ESTEX -# program — used both by libc feature tests under tests/ and by real -# applications under examples/. +# program. Приложение может лежать вне репозитория тулкита. # # Usage in a per-program Makefile: # -# PROJ_ROOT := $(abspath $(CURDIR)/../..) +# SPRINTER_ROOT := /путь/к/C-Compiler # EXAMPLE := my_program # base name (matches my_program.c) # # # Optional overrides (any combination): @@ -28,24 +27,43 @@ # all build $(EXAMPLE).exe (default) # clean remove build artefacts # floppy build the example and pack it (alone, plus EXTRA_DATA) into -# mame/v306/IMG/mc.img — useful for trying a single program -# without rebuilding every example. Top-level `make floppy` -# (in the repo root) still packs all examples. +# build/media/$(EXAMPLE).img внутри приложения. # run floppy + launch MAME +ifeq ($(strip $(PROJ_ROOT)$(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT — путь к установленному Sprinter-CC) +endif +ifeq ($(strip $(PROJ_ROOT)),) +PROJ_ROOT := $(SPRINTER_ROOT) +endif +ifeq ($(strip $(SPRINTER_ROOT)),) +SPRINTER_ROOT := $(PROJ_ROOT) +endif + PYTHON ?= python3 SPRINTER_CC := $(PROJ_ROOT)/bin/sprinter-cc MKEXE := $(PROJ_ROOT)/toolchain/mkexe/mkexe LIB := $(PROJ_ROOT)/lib/sprinter.lib -MAME_DIR := $(PROJ_ROOT)/mame/v306 -FLOPPY_IMG := $(MAME_DIR)/IMG/mc.img -# ?= — приложение со своим каталогом выхода (applications/SprPoP) держит -# образ у себя и связывает его с MAME символьной ссылкой. -HDD_IMG ?= $(MAME_DIR)/IMG/test_hdd.chd -MAKE_DISK := $(MAME_DIR)/make_disk.py +# Временный fallback для старого общего дерева. После переноса нет MAME +# внутри SDK, поэтому внешний проект задаёт MAME_HOME или отдельные пути. +MAME_HOME ?= $(if $(wildcard $(PROJ_ROOT)/mame/v306/mame.arm),$(PROJ_ROOT)/mame/v306,) +MAME_BIN ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/mame.arm,) +MAME_ROMPATH ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/roms,) +MAME_DSS_IMAGE ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/IMG/dss171u.img,) +MAME_SYSTEM_HDD_IMAGE ?= $(if $(strip $(MAME_HOME)),$(MAME_HOME)/IMG/sp_hdd_sys.chd,) +MAME_BIOS ?= v3.06 +FLOPPY_IMG ?= $(CURDIR)/build/media/$(EXAMPLE).img +HDD_IMG ?= $(CURDIR)/build/hdd/$(EXAMPLE).chd +MAKE_DISK := $(PROJ_ROOT)/toolchain/make_disk.py MAKE_HDD ?= $(PROJ_ROOT)/toolchain/make_hdd.sh -RUN_MAME := $(MAME_DIR)/run_mame.sh +RUN_MAME := $(PROJ_ROOT)/toolchain/run_sprinter_mame.py +CHDMAN_BIN ?= chdman +MAME_PROFILE_ARGS = --mame-home "$(MAME_HOME)" --mame-bin "$(MAME_BIN)" \ + --mame-rompath "$(MAME_ROMPATH)" \ + --mame-dss-image "$(MAME_DSS_IMAGE)" \ + --mame-system-hdd-image "$(MAME_SYSTEM_HDD_IMAGE)" \ + --mame-bios "$(MAME_BIOS)" # Optional knobs — see top of file. MEMORY ?= tiny @@ -141,26 +159,26 @@ $(LIB): clean: rm -rf $(if $(strip $(BUILD_DIR)),$(BUILD_DIR),.sprinter-cc-* $(EXE)) -# `make floppy` packs ONLY this program (+ optional EXTRA_DATA files) into -# the MAME floppy image, replacing whatever was there. Handy for trying a -# single program without rebuilding everything. +# `make floppy` создаёт носитель только этого приложения, без записи в MAME. floppy: $(EXE) - python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXE) $(EXTRA_DATA) + $(PYTHON) $(MAKE_DISK) "$(FLOPPY_IMG)" $(EXE) $(EXTRA_DATA) @echo @echo "Floppy ready: $(FLOPPY_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA))) " - @echo "Run: cd $(MAME_DIR) && ./run_mame.sh" + @echo "Run: make run MAME_HOME=/путь/к/MAME/runtime" run: floppy - cd $(MAME_DIR) && ./run_mame.sh + $(PYTHON) $(RUN_MAME) $(MAME_PROFILE_ARGS) --floppy "$(FLOPPY_IMG)" -# `make hdd` packs this program (+ optional EXTRA_DATA files) into the MAME -# HDD image mounted as disk D: (-hard2 test_hdd.chd). Гораздо быстрее FDD — -# используется MCP-мостом к MAME (run_bridge.sh). После пересборки образа -# MAME ОБЯЗАН полный рестарт (chdman -f = новый inode; см. memory). +# `make hdd` кладёт носитель в build/ приложения. Для нового inode после +# пересборки образа запущенный MAME должен быть перезапущен. hdd: $(EXE) - $(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") $(HDD_IMG) $(HDD_PACK_ARGS) + mkdir -p "$(dir $(HDD_IMG))" + CHDMAN_BIN="$(CHDMAN_BIN)" $(MAKE_HDD) $(if $(strip $(HDD_DEST_DIR)),--dest "$(HDD_DEST_DIR)") "$(HDD_IMG)" $(HDD_PACK_ARGS) @echo @echo "HDD (D:) ready: $(HDD_IMG) (with $(EXAMPLE).exe$(if $(EXTRA_DATA), + $(EXTRA_DATA)))" - @echo "ВНИМАНИЕ: перезапусти MAME (run_bridge.sh) — образ пересобран." + @echo "ВНИМАНИЕ: перезапусти MAME — образ пересобран." -.PHONY: all clean floppy run hdd +run-hdd: hdd + $(PYTHON) $(RUN_MAME) $(MAME_PROFILE_ARGS) --hdd "$(HDD_IMG)" + +.PHONY: all clean floppy run hdd run-hdd diff --git a/applications/PoP/.gitignore b/applications/PoP/.gitignore new file mode 100644 index 0000000..fad014b --- /dev/null +++ b/applications/PoP/.gitignore @@ -0,0 +1,29 @@ +# Референсные клоны и оригинальные игровые данные — не часть порта. +SDLPoP/ +PR/ +mininim/ +Prince-of-Persia-Apple-II/ +MSDOS/ +PoP1_DOS_music/ +R1/ +local.mk +build/ +.sprinter-cc-*/ +.resource-stamps/ +*.exe +*.asm +*.lst +*.lk +*.ihx +*.noi +*.sym +*.map +*.rel +*.cdb +*.mem +*.rst +__pycache__/ +*.py[cod] +.DS_Store +._* +.vscode/ diff --git a/applications/PoP/.python-version b/applications/PoP/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/applications/PoP/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/applications/PoP/LICENSE b/applications/PoP/LICENSE new file mode 100644 index 0000000..a738358 --- /dev/null +++ b/applications/PoP/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sprinter C Compiler contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/PoP/bgtest/Makefile b/applications/PoP/bgtest/Makefile index cd2bee0..505393f 100644 --- a/applications/PoP/bgtest/Makefile +++ b/applications/PoP/bgtest/Makefile @@ -6,7 +6,6 @@ # # Данные фона генерит toolchain/pop_pack_bg.py в ../poc/res/bg/. -PROJ_ROOT := $(abspath $(CURDIR)/../../..) EXAMPLE := bgtest MEMORY ?= huge EXTRA_FLAGS ?= --gfx 256 @@ -17,11 +16,11 @@ BG_DATA := $(BG_DIR)/pop_env0.atl $(BG_DIR)/pop_env1.atl $(BG_DIR)/pop_env2. $(BG_DIR)/pop_wall.atl $(BG_DIR)/pop_fore.atl $(BG_DIR)/pop_bg.pal EXTRA_DATA := $(BG_DATA) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk # Ассеты фона: пересобрать пакером, если исходники поменялись. -$(BG_DATA): $(PROJ_ROOT)/applications/PoP/toolchain/pop_pack_bg.py \ - $(PROJ_ROOT)/applications/PoP/toolchain/render_room.py - cd $(PROJ_ROOT)/applications/PoP/toolchain && python3 pop_pack_bg.py +$(BG_DATA): $(POP_ROOT)/toolchain/pop_pack_bg.py \ + $(POP_ROOT)/toolchain/render_room.py + cd $(POP_ROOT)/toolchain && python3 pop_pack_bg.py $(EXAMPLE).exe: $(BG_DATA) diff --git a/applications/PoP/coltest/Makefile b/applications/PoP/coltest/Makefile index 4ee59f6..eb55155 100644 --- a/applications/PoP/coltest/Makefile +++ b/applications/PoP/coltest/Makefile @@ -1,6 +1,5 @@ # coltest — прототип вертикального accel-copy (gfx_blit_cols) + флип. -PROJ_ROOT := $(abspath $(CURDIR)/../../..) EXAMPLE := coltest MEMORY ?= huge EXTRA_FLAGS ?= --gfx 256 -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk diff --git a/applications/PoP/poc/Makefile b/applications/PoP/poc/Makefile index a2e46ad..5a3a809 100644 --- a/applications/PoP/poc/Makefile +++ b/applications/PoP/poc/Makefile @@ -20,7 +20,6 @@ # бы уехал). sprintf() заменён на ручное hex-форматирование пути в # tile_atlas_load() (единственный потребитель printf, ~2.9КБ). -PROJ_ROOT := $(abspath $(CURDIR)/../../..) EXAMPLE := poc MEMORY ?= huge EXTRA_FLAGS ?= --gfx 256 @@ -28,9 +27,9 @@ EXTRA_SRCS := room.c TILE_ATLASES := res/tiles/tile01.atl res/tiles/tile14.atl res/tiles/tile03.atl \ res/tiles/tile13.atl res/tiles/tile0e.atl res/tiles/tile0b.atl EXTRA_DATA := tools/kid.atl tools/room.pal res/room1.dat $(TILE_ATLASES) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk -LEVEL1_BIN := $(PROJ_ROOT)/applications/PoP/SDLPoP/data/LEVELS/res2001.bin +LEVEL1_BIN := $(POP_ROOT)/SDLPoP/data/LEVELS/res2001.bin tools/kid.raw tools/kid.pal: tools/gen_kid_placeholder.py cd tools && python3 gen_kid_placeholder.py diff --git a/applications/PoP/poc/tools/build_room_palette.py b/applications/PoP/poc/tools/build_room_palette.py index 6ffe9e7..f74a7a8 100644 --- a/applications/PoP/poc/tools/build_room_palette.py +++ b/applications/PoP/poc/tools/build_room_palette.py @@ -22,9 +22,10 @@ kid.pal). import subprocess import sys from pathlib import Path +import os HERE = Path(__file__).resolve().parent -PROJ_ROOT = HERE.parents[3] +PROJ_ROOT = Path(os.environ["SPRINTER_ROOT"]).resolve() TILES_DIR = HERE.parent / "res" / "tiles" PNG_STRIP = PROJ_ROOT / "toolchain" / "png_strip.py" MKATLAS = PROJ_ROOT / "toolchain" / "mkatlas.py" diff --git a/applications/PoP/poc/tools/build_tile_atlases.py b/applications/PoP/poc/tools/build_tile_atlases.py index cb12893..f085ef8 100644 --- a/applications/PoP/poc/tools/build_tile_atlases.py +++ b/applications/PoP/poc/tools/build_tile_atlases.py @@ -15,9 +15,10 @@ import re import subprocess import sys from pathlib import Path +import os HERE = Path(__file__).resolve().parent -PROJ_ROOT = HERE.parents[3] # .../C-Compiler +PROJ_ROOT = Path(os.environ["SPRINTER_ROOT"]).resolve() TILES_DIR = HERE.parent / "res" / "tiles" OUT_DIR = HERE.parent / "res" / "tiles" PNG_STRIP = PROJ_ROOT / "toolchain" / "png_strip.py" diff --git a/applications/PoP/poc/tools/gen_kid_placeholder.py b/applications/PoP/poc/tools/gen_kid_placeholder.py index 2f6363e..fb5dca1 100644 --- a/applications/PoP/poc/tools/gen_kid_placeholder.py +++ b/applications/PoP/poc/tools/gen_kid_placeholder.py @@ -18,7 +18,7 @@ third_party/16x16-RPG-characters, один персонаж (индекс 0) т """ from PIL import Image -SRC = ("../../../../third_party/16x16-RPG-characters/sprites/" +SRC = ("../../third_party/16x16-RPG-characters/sprites/" "old-style/02-bard.png") CHAR = 0 # какой из 8 персонажей листа берём diff --git a/applications/PoP/roomtest/Makefile b/applications/PoP/roomtest/Makefile index 9bfde2a..b471b62 100644 --- a/applications/PoP/roomtest/Makefile +++ b/applications/PoP/roomtest/Makefile @@ -1,7 +1,6 @@ # roomtest — проверка порта статического фона PoP (Шаг 3): реальная # композиция слоёв (pop_bg.c) для комнаты 1. --memory huge --gfx 256. -PROJ_ROOT := $(abspath $(CURDIR)/../../..) EXAMPLE := roomtest HDD_IMG := $(CURDIR)/build/hdd/roomtest.chd @@ -276,9 +275,9 @@ HDD_PACK_ARGS := $(EXAMPLE).exe \ $(foreach f,$(PV_DATA),PV:$(f)) \ $(foreach f,$(LVL_DATA),LEVELS:$(f)) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk -TC := $(PROJ_ROOT)/applications/PoP/toolchain +TC := $(POP_ROOT)/toolchain # Упаковщики создают сразу ГРУППУ файлов. Обычное multi-output правило # заставляло GNU make при `-B` запускать один и тот же упаковщик отдельно для @@ -389,25 +388,25 @@ $(KID_BIN_STAMP): $(TC)/pop_extract_kid_data.py | $(RESOURCE_STAMP_DIR) kid_data.h $(KID_BIN): $(KID_BIN_STAMP) @test -f $@ || { $(MAKE) -B $(KID_BIN_STAMP); test -f $@; } -$(FONT_STAMP): $(PROJ_ROOT)/toolchain/pop_extract_font.py \ +$(FONT_STAMP): $(POP_ROOT)/toolchain/pop_extract_font.py \ $(CURDIR)/../SDLPoP/src/menu.c $(CURDIR)/../SDLPoP/src/seg009.c | $(RESOURCE_STAMP_DIR) - cd $(PROJ_ROOT) && python3 toolchain/pop_extract_font.py + cd $(POP_ROOT) && $(PYTHON) toolchain/pop_extract_font.py touch $@ $(FONT_ATL) pop_font.h: $(FONT_STAMP) @test -f $@ || { $(MAKE) -B $(FONT_STAMP); test -f $@; } -$(TITLE_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_title.py \ +$(TITLE_STAMP): $(POP_ROOT)/toolchain/pop_pack_title.py \ $(CURDIR)/../SDLPoP/data/TITLE/res51.png \ $(CURDIR)/../SDLPoP/data/TITLE/res52.png \ $(CURDIR)/../SDLPoP/data/TITLE/res53.png \ $(CURDIR)/../SDLPoP/data/TITLE/res54.png \ $(CURDIR)/../SDLPoP/data/TITLE/res55.png | $(RESOURCE_STAMP_DIR) - cd $(PROJ_ROOT) && python3 toolchain/pop_pack_title.py + cd $(POP_ROOT) && $(PYTHON) toolchain/pop_pack_title.py touch $@ $(TITLE_ATL) $(TITLE_DIR)/title.pal: $(TITLE_STAMP) @test -f $@ || { $(MAKE) -B $(TITLE_STAMP); test -f $@; } -$(PV_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_intro.py \ +$(PV_STAMP): $(POP_ROOT)/toolchain/pop_pack_intro.py \ $(CURDIR)/../SDLPoP/data/TITLE/res41.png \ $(CURDIR)/../SDLPoP/data/TITLE/res42.png \ $(CURDIR)/../SDLPoP/data/TITLE/res43.png \ @@ -429,7 +428,7 @@ $(PV_STAMP): $(PROJ_ROOT)/toolchain/pop_pack_intro.py \ $(foreach n,852 853 854 855 856 857 858 859 860 861 862 863 864,$(CURDIR)/../SDLPoP/data/PV/res$(n).png) \ $(CURDIR)/../SDLPoP/data/PV/res952.png \ $(foreach n,151 152 153 154 155 156 157 158 159,$(CURDIR)/../SDLPoP/data/PRINCE/res$(n).png) | $(RESOURCE_STAMP_DIR) - cd $(PROJ_ROOT) && python3 toolchain/pop_pack_intro.py + cd $(POP_ROOT) && $(PYTHON) toolchain/pop_pack_intro.py touch $@ $(PV_ATL) $(PV_DIR)/story.pal $(PV_DIR)/pv.pal: $(PV_STAMP) @test -f $@ || { $(MAKE) -B $(PV_STAMP); test -f $@; } diff --git a/applications/PoP/roomtest/tests-host/Makefile b/applications/PoP/roomtest/tests-host/Makefile index ccaa697..039ee15 100644 --- a/applications/PoP/roomtest/tests-host/Makefile +++ b/applications/PoP/roomtest/tests-host/Makefile @@ -1,6 +1,12 @@ # Модульные тесты движка roomtest под ucsim_z80. Обвязка общая (testkit/), # здесь — только сами наборы и список модулей, которые в них линкуются. -TESTKIT := $(abspath $(CURDIR)/../../../../testkit) +POP_ROOT := $(abspath $(CURDIR)/../..) +-include $(POP_ROOT)/local.mk +SPRINTER_ROOT ?= $(if $(wildcard $(POP_ROOT)/../../bin/sprinter-cc),$(abspath $(POP_ROOT)/../..),) +ifeq ($(strip $(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT — путь к установленному Sprinter-CC) +endif +TESTKIT := $(SPRINTER_ROOT)/testkit ENGINE_DIR := $(abspath $(CURDIR)/..) # t_phys гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации diff --git a/applications/PoP/sdk.mk b/applications/PoP/sdk.mk new file mode 100644 index 0000000..ea08f79 --- /dev/null +++ b/applications/PoP/sdk.mk @@ -0,0 +1,11 @@ +# Привязка архивных PoP-проб к установленному Sprinter-CC. +POP_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +-include $(POP_ROOT)/local.mk +SPRINTER_ROOT ?= $(if $(wildcard $(POP_ROOT)/../../bin/sprinter-cc),$(abspath $(POP_ROOT)/../..),) +ifeq ($(strip $(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT или создайте PoP/local.mk) +endif +PROJ_ROOT := $(SPRINTER_ROOT) +export SPRINTER_ROOT MAME_HOME MAME_BIN MAME_ROMPATH MAME_DSS_IMAGE +export MAME_SYSTEM_HDD_IMAGE MAME_BIOS CHDMAN_BIN +include $(PROJ_ROOT)/app.mk diff --git a/applications/PoP/third_party/16x16-RPG-characters/LICENSE b/applications/PoP/third_party/16x16-RPG-characters/LICENSE new file mode 100644 index 0000000..604209a --- /dev/null +++ b/applications/PoP/third_party/16x16-RPG-characters/LICENSE @@ -0,0 +1,359 @@ +Creative Commons Legal Code + +Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined below) for the purposes of this + License. + c. "Creative Commons Compatible License" means a license that is listed + at https://creativecommons.org/compatiblelicenses that has been + approved by Creative Commons as being essentially equivalent to this + License, including, at a minimum, because that license: (i) contains + terms that have the same purpose, meaning and effect as the License + Elements of this License; and, (ii) explicitly permits the relicensing + of adaptations of works made available under that license under this + License or a Creative Commons jurisdiction license with the same + License Elements as this License. + d. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + e. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, ShareAlike. + f. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + g. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + h. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + i. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + j. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + k. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(c), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(c), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under the + terms of: (i) this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible + License. If you license the Adaptation under one of the licenses + mentioned in (iv), you must comply with the terms of that license. If + you license the Adaptation under the terms of any of the licenses + mentioned in (i), (ii) or (iii) (the "Applicable License"), you must + comply with the terms of the Applicable License generally and the + following provisions: (I) You must include a copy of, or the URI for, + the Applicable License with every copy of each Adaptation You + Distribute or Publicly Perform; (II) You may not offer or impose any + terms on the Adaptation that restrict the terms of the Applicable + License or the ability of the recipient of the Adaptation to exercise + the rights granted to that recipient under the terms of the Applicable + License; (III) You must keep intact all notices that refer to the + Applicable License and to the disclaimer of warranties with every copy + of the Work as included in the Adaptation You Distribute or Publicly + Perform; (IV) when You Distribute or Publicly Perform the Adaptation, + You may not impose any effective technological measures on the + Adaptation that restrict the ability of a recipient of the Adaptation + from You to exercise the rights granted to that recipient under the + terms of the Applicable License. This Section 4(b) applies to the + Adaptation as incorporated in a Collection, but this does not require + the Collection apart from the Adaptation itself to be made subject to + the terms of the Applicable License. + c. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Ssection 3(b), in the case of an + Adaptation, a credit identifying the use of the Work in the Adaptation + (e.g., "French translation of the Work by Original Author," or + "Screenplay based on original Work by Original Author"). The credit + required by this Section 4(c) may be implemented in any reasonable + manner; provided, however, that in the case of a Adaptation or + Collection, at a minimum such credit will appear, if a credit for all + contributing authors of the Adaptation or Collection appears, then as + part of these credits and in a manner at least as prominent as the + credits for the other contributing authors. For the avoidance of + doubt, You may only use the credit required by this Section for the + purpose of attribution in the manner set out above and, by exercising + Your rights under this License, You may not implicitly or explicitly + assert or imply any connection with, sponsorship or endorsement by the + Original Author, Licensor and/or Attribution Parties, as appropriate, + of You or Your use of the Work, without the separate, express prior + written permission of the Original Author, Licensor and/or Attribution + Parties. + d. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of the License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/applications/PoP/third_party/16x16-RPG-characters/README.md b/applications/PoP/third_party/16x16-RPG-characters/README.md new file mode 100644 index 0000000..b3b78a0 --- /dev/null +++ b/applications/PoP/third_party/16x16-RPG-characters/README.md @@ -0,0 +1,27 @@ +# 16x16 RPG characters `v3.0` + +16x16px RPG character sprite sheet for up to down games. + +This asset pack has been downloaded from https://route1rodent.itch.io/16x16-rpg-character-sprite-sheet + +Adapted from opengameart.org 's "[NES-Style RPG Characters](https://opengameart.org/content/nes-style-rpg-characters)" and "[More NES-style RPG Characters](https://opengameart.org/content/more-nes-style-rpg-characters)", with additional content made by [@route1rodent](https://route1rodent.itch.io). + +## License + +16x16 RPG character sprite sheet (c) by @route1rodent + +"16x16 RPG character sprite sheet" is licensed under a +Creative Commons Attribution-ShareAlike 3.0 Unported License (CC BY-SA 3.0). + +You should have received a copy of the license along with this +work. If not, see http://creativecommons.org/licenses/by-sa/3.0/. + + +## Credits + +Maintained by @route1rodent: + +- itch.io: https://route1rodent.itch.io +- Twitter: https://twitter.com/route1rodent +- Github: https://github.com/itsjavi +- Blog: https://blog.itsjavi.com diff --git a/applications/PoP/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png b/applications/PoP/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png new file mode 100644 index 0000000000000000000000000000000000000000..98bf1c74f65525aaaca2783040619e148fe7ecca GIT binary patch literal 7830 zcmaKx2T)U6*Mxh0ivno+}xo7xIIK3I7tp0eWq&p!2cXqPLt_0DPqqI)M zT6P*QKyy(WgMhQs2aldp5+<0;;#U2`9J`t-3xBii+67I_gw>Z?1{@ z31QGVX`4Px5n_L9N(?taD}P5V<%S#k*~Pv=FW(1TOYC>>pHKcy!q{gIk#<}@HEFCN zh%1{b-FK+sG*!^D%No6JoUukvyvg4&MNTv%_@d2Ook27#gZ@*0VZ% zWBbocJMWUW8uB2a4{2VN%vJ7hjMqs;6cBoEWq?jy zTiJs7`GulCsx+BM?ZKxz|hA2n421l#jg0>eui&$OnpI zGo6;2-MtdAamc|D zYT5m6Su*#;pE=&3JS8E_LU0Yq2dgil+IU!6-`9#vcBMV$ED6x?RHVIW57q{X(9#g& z0fhbS&c{GQ&NAF{MQ9QCd}|m*)qY$pyGB4%J$%>y>QHMA#mWc-mv@-GTG25tI5!;cRL_PuqAh@aDudP(a)rnINtB2^X^PfJ~Cs=~KgH z4=1|z=^L_%3t<87ta%W(4$mqkCr`ZX#?~zp}HXf?&A8{#$H=n4jDa@g!kRA zj%z2Q<4Q!F_(WSnR!Nh3Ng7#Gv)|AYFhS8hs~Jobv%Bl8bbXUX(VU`T6qGjei%PzG zXWq_sg+woIkLq>9r8O_xjMgj6GYCf#F0nqnarHu81l;Cu?3q$%fW2s~Ye+~?#t9D@ z=$QEf`0@)V!v-hWdkU!s^P#N%+?SH+h5GT`fUYlxlzlX{tJ&H}Zn%AXCoHXHxfBHT zqWTl5Q>5axA>9waViB;>qqV-2lA%lR*2sPm@cvMm+;g$#oesW^RrLCR`-T?o$>{l( zEz$}M&}_3=w_W@=!c43AsfJ_%f0h)(dkNXbm9ai_6Pt9k8$aF9$*_Q>m}$ZK8R_;3 z^irDHl#{=RwbU$enQRxfUyXhWCmgC!@hv?k@rnsvLbkt1MRMcEqd5DRNW2njv$_Z> zJCm=w&CH{es7~h|w4ozx}vcG-j3j1i9;)%9b#v)h~gPfyyn#+?yv|@=R{6)M|vtXh4XhL z4mIS7Pd7}zU8nn8qzTY;*T2rQ%_$@y?l{VXIl$hoGpXZ=2tu$AKRt27i}8T`xS$Y; zk4inImVwUALWJ#Zc6*+Nal*uM+kIk%Fg9I8)bYfnZFgk$piduE8a)kaNj@6X(Nvrz z>~@D#7PPudi!M3`lYyNr+jvare2SdP*x`OG;}fqVPD|x!P5QXwH|Vmh9i+82JdQ*Z zMv+nEr9;z8!(|-uC`$(|5~{I)^-jOWE1W3@o<1>}<}(2K)u`|~8ALt;YHrea>xPzf z@uXU@HR`x6d$`T|)qU8M?Nj$2trJ{#u}JyKJ0K0yk}*YukHq0Hfi+!Lz`(#mDubrPs8%)sw9l&R3f$bj*;$S8Hcur=xi#jwDXcV zPFBtAmvZmX%RCDr*94={1*!KQG!`xtiJlYxYgwD@0nW~vYqqeic`~L*R6B_pc0VGI zb{NE%83TUygAa06AmO{A5z`?lbK%vZfCNjK>oV>(MSLyR+(SKsJVl`QS#BtZHFrd( zbcz^Xb?NUMj-?>?C44N{zkJ#^U3oV~-cv2$3E0v#1)t&v6=o#kv#+D9DfbR4Z{16( zMk|S^wIQT0_XhXoTBy*ZFq#R@H3Sn}%gZJ#bYf z_TPe|duBc;zfn$9J%Z-@k@u9j^wm3uI@osp1uq_JNd;tvA-HDc?^?YmTd%#(AOCOw z@?;%kO%7}P+*EhV6}2;9jFH>qp5TU3OjY(pMN*k^1}yi+azSOy;hdA))r6|gvthlp zH#QnqVrK$a3*{rMr3a0IXpco&R=o}IXS{{nw>u-<21x<#^vTaKH|o;DOv@}7io#GO zp_@O}V|Ph{^L29fqxq#i{4}E~>&kwvttiagSUth*qJ{e0Wg)MnZbGyU-$18NYuF}Y z+t6+}GrXK`3gd*RxMFQ-e2tR~3x>h=uxg4=(P4Ki5*3^P%oEK7y2sOPD5ef)FxDJH zZSpR7K9HWK3TTLjTDQI5lUq-oKElse(&0%xl;V;J@-a%*?k%y@m$n z0NU|6zaTMD3ETTzQ{Ow{>NZxzQ1vI2htf)FS<5gbVJhJ@bj+!9Imm;ElU4t8!|P%6 z^VX+M)ppB^b+#S$B^`NO(#^rTA(EtdaehSOx=l8KM;_mi!f~Uf9oxtw?{h)4vDPLL z1g?iFZym%)Ms)P5qn6l5c0XnzWB8pnkG(0>>2)df3pdmR?6`p2^27`JQBdP+#$Qqp zb2E-Ixsw%_CaiJ7%}Pl=MIcAmh7L3!zgm@VQ0ns>sg7^h(<<3amUv+gV1@t*VUYuf zJjK#_TDCLv1R}IiT)UJ){vX0E{+84z%Z2?Egz>H`)1_Bu%E#V5_nyFsQNhMNz$wNh z`xtFqMrg!Q_!)%LEqLEu?vq#q?%q+|#d1)^HvbBnwXlE-+(-r$zRUhhrJ|sH-0Y9} z`XAOCg+6FHpMtR0J_Qo3qRY}AfY0Gt;D^Kc8ea)ECm%Sj#p+VTxoDOxZmrTqSi>Uj z6j)p=JWtGv)8iZ1QKm@M*QoJ}r^}9hK_EN=mA~Aggvz|ahxQQoy~lyhCEAI+dCq2% zcaNY?L_7u|aE1~uO4pw)lgA~s`y&k(>yVN{o(FkWK^`7Ucp@)oz`%$3zz$yD|I5o^ zh8`>>9q5L(dCFb&+jjmvWD2VQvjTzT8!L9pOUlK%%2Z8Sbik@L%cFLFvNC=uXCNzn zrA?k#HCjh6?u5FmK;`W7>BW+!e6TtB zr*GwVw{F<}5u6J=f6GAskmtCf#nC?u-7cnUJ$d#-K|Hk7MD@r$%z*<>tDSoR$dmB4SOoRnji z?Zx~)$<+vJuf_lixBtdmp?JaGfrdbs%{tiWwXcw+^Rosc~&&$izFguFn;3LQgGQ`ziPz<+h8fD z&l?dzoeq! z)Y@t|SIz=*)1l2~#zDu+}FWXB#&6Zm+73EKEp|^PpXJO?UUN zks!mtZt&pBhs*#Py@t8w{0$VMX>-dNjuKb&owo_o1mN-;8)_z5bwnJt+LCn)@edty zfRz2kA1+W`!l-owaeI}DzcnRTU^HicBxOo{+dF`?rEIzuC6Iw$ZkfLD>daO^Z~A@j z+m2q0OyCYGV8fFckogxpI~r0QnhdHTHk>}^iU+3(T?m%zY%JbQW#BjeMd9+fe6|<} zInr}$RkE_%=#BvtxlQJeSv6C9060}`GAo; zLf_-AYPHs;eT_mhoC>E0)e4uvw26A={%Gb_IXrbQlXi7IJ)K3L%=#G-+Czp{D|{>H z&VP_EUQTM5s-tTt~AVJ=GLpEs{ETedW4lNnK%kd-(*A z7SL>L&_X2V46JaQ{iyv=Ee9NBXN*+G^2uA}a~D2oW$Ymyz!LHB@2dx1dX_$B!;EQh z7}MOc8q)SpHtV}4f#OsE+2T77WQab=a84>E=gn7#2Q<_Mf)$P?$`9Xv7k-?fxPkxl zaJ?oD6Q4ptXUpDVs&g|}_W*SF=Oe^Ud|?^L{Tn6%FJ`T?O)R3M8Q%21q*SnXG--k* zKG?*xA=Dm0IV_UqD+t6daXn!r1Ev@-~nQq76VPNm=clD|!Jp!b`7|s3wHVD`o$MscXNh z&bG8Z0UCA>U~Bxv)h;wooYix4iTwj#P&=yUy?7lv9?7x?j-mkd9Nl%ra4t8Pn#08# zO}!oU+hJW;m1h0e7zdL4@-JEifvbKyz3BKGInT!rp=73AlHc3Ej`5CDd!tk0jM_1% z7HwbgT|V$qt*SRUz`wxkSJf#b;fLR1_aT7+Mpu5qFxfxwR>sfaao6w0!M< zTPMp=P3+B=RlveO(NrSGhIH6E@J6E&IC~*b`0)FC1veX$BmM6aKA#UvCREb>*bR;E z-<>x8GpmNKYVQW>Nb;8BLqesXY)e1V+@a=9(UM??_B?1n^_aM?_~(}|(sw5Q6JEz$ zT9*iyL`9|@Bi^0PXrN9`o3!VNdQxo&-?dtG4d3~E<=1Xsz*w}Nzb-Ax^w9A7@@Vhk zJsm*~(Y<7x0}D!fRX#2jeN!xj_WB^Bbe>sgsL6BNRA$QE|N5dUN?)^p+36C!6k5SBr^5kP+G0)Wj#u_@4T&Am4x)KJuKT+t`I#C;BlttwZkSb zd7CJ^s2WmNS}yr3ANowtlx?iV@!&>P*yNlnYf)6ObWhUcuJwk`KS--}<_FTgW7T^h z>Gs!WnrF5?9F}@XV_rfF$8d*RaM`DKbaQHsq=`@6fU2LsrpOA znHUVBk>%;9QU?+}PNoEWKMKEjAS2Ukv(deKkCOfhQjlyEO{j(xUU3n8B9X;1*yt-) zIlNHdQRI@R7cB5H_FE8(TN#W;ALWcn{{+wjYOnY~R?w>C!TmNwt4VeC2|ZNef(x%e zZ^Gc2JIu|Lgk<1 zxWj^XnUjNmV{6>pDG$l+yRR`Gu`FYqdK}k{Lt=Xp1~(h7HIZqFtk|ycy??niheX0q z2D$7SKV9MNKy=E3IAK!_{#XeTZrl#s%YvVtoUEfdL^XwBg)GW4P3Ipbl{azUpM=D@ zd!vIdOSK_zfsVP@OUX-Ll$0Si6M(Eud3O+uH17LvWc7hju5L08x?3VO*Elb+C>9LH zUUJF*&1y!TWp3awTeZt-6_QzOol*PS4SM1lmXPl4#t+deTOv#4stPaB@)7=QbGD-L zOsh?dWW_bH3|=AZKSJrI4{jy9vhPdzQtQetp*MPxEPI&vN=V<;6$U?n5Re&4OGh*9 z7Z+`w`TeR%+n!f~8D48#>^gNe4dc)*-lZ1Bzl>yh1vOeoE;`-reQk6tRjd8a^7f-+0lR76fJOh|G zUcpl*<57*f8@tc`;_AtemAciRB&%IbFXeTVc(_h8F#cPb-1SQAcWRB!{0BjWjc?Jp zcN3#NzrFGQqAIf}Z}$s^G2Q2FNDZ@iVpHflsz>$TK$myW$3I-moniofaH>m^YvR+o zRqc}6S|bj0@F5RQMv8Mv5ivdKU1&t&&ys(e1Q`dq`m$J+bww}0--QBHxA19fN!ti) z3nol6LU@0_4-iJtFaH6fYyJA`Y!%Q>vyQ0R*ALiA_5h_cnU_BZmXDgZ2e4{%mU6x} zbF&68@hcC@)M-X^Mkee5o;HMUN}XJC0}U+UPA^R^I!TSQj~(C1qgjCDf^s>6DbwcX zdIq^Ndpjis%f93pumHI6{WWQ{a!{hYrxv7H><64KK+D7HjCH>t1Ld4J86&;vg9v&2 z$g(L+nALZhxW~KHtA-qELrd^}R##YFvhk;=?&qMOlGXF#v}wDjDm7Uet7!fOq+l#E z*zsR-%}_*maKdEmS4xaISjJ{zg;C{u)U`8=Jgx;;nacz2U?kR9TM;h%KLc#i%q|AN z6lC8YGty6qWuyk0mKmQrql9&5cXw+SOlo-uLm9XY~H?f_> z+|j2-9g(FKW(M&M-YN-dyr(?XgTR7fWik^mQTx;2rNOn|hrxS^~z2bvaJqoVM=HML%XMg?Mp$5PURc|31Ji?%PFTiy z3fWp!@RVPm<=S0Il*upsEdNAsa#izyRD)SOL zmX)Wp-72T$M*VE$oP&WQ%SJ63U70floe6&ftqfXcTF!r)CCgjjOk;U5>bZ@l!w4@E z^yl_uO#tze!bKA0XeFfGgcx+jWJb0t8eyv;6#TgTv^H)jYicxQ`miH?Gp=rywq37X zC)JQ(XruyesCKgFs-x4Oj+X{FtRAhgjtPoIRQks2rba#;KU9}9E76tC(@GA=tZeu= z_GAYABfVSIkd8X-K*htw0%b(~_hf6U%h+2tC4=L?>~UNy0=)>;m}2ew8Cj;H&*K+I zY!a|a8sCXHn;oJ*+J)s1mDTe!1V__*@lr9YF0y{RNxFCvA`34I&RYG#=uU~PK&#k{ zKF|j1T_fX4L2)cnOca{CM(-_#)Y1i-q3dj7G*gX*jz1iEs0lj&AjrOxn1Y%$(|dQ% z_*@$T>g_exWA`d0{g5;S_Y{~>Z+JE&f)U>82JJ% ztAq2Vj5r{ZmvEPG)0*e_U2Ds|USgeFIB)*n1WUYF{=6UjdVYSj%1W4cJ)ykBO^V7BcUK?YRI1~ z+s{}PsaS6Linrj$+dDB@Dh&;#A%^{tRi7+L%%aSMnVgA@eFFE$TTi0=Dc*HwRo+h* zY7+Zc9ZHsmJ+Mrm+r~cQ+C*V`9l{POj3lU@{8qg^V-~P@xTei+IUhB|$0@)aLFZ!_ zsI}^b3ik~t1s>%+=5$j{%&IsE^;>EtU3WD0_REun`QlOiYoAH1cEHCJhU48|6hqnm z#Z`TB{Ph2dvoO};>HJTOon=j@mUPGj29er%s{{%d^&?0YP9xYHQK1(U?n1hpisC6h6J-r!QU8ZPL=dmq!e zX73!?yySG(^)HOw`&#mBa-bEz_=kS`ithT8ZS~26Wnn80A%E%i@qqcV%kKKe)Nj8U zS%x)Poo?xVG{w^#vxGJzr*G2Q0z*ouyJ8kI;!_`hirKgnMwn@0loDGCX%Jib6JWuU zOQTM+Wc^QNo+@k z-UTr#Fzy~(h9|o~-VhM&RL@ONGj{QXa&?IJVT7XJ9^ZT`g@ S$U0~S7+g2jE&I(e>i+;CrcmSn literal 0 HcmV?d00001 diff --git a/applications/PoP/toolchain/pop_extract_font.py b/applications/PoP/toolchain/pop_extract_font.py new file mode 100644 index 0000000..6c6d284 --- /dev/null +++ b/applications/PoP/toolchain/pop_extract_font.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""pop_extract_font.py — SDLPoP-шрифты в один Sprinter .atl-ресурс. + +Источник истины — оригинальные встроенные шрифты SDLPoP: + * hc_small_font_data[] из menu.c — пункты меню; + * hc_font_data[] из seg009.c — крупные сообщения/заголовки. + +В исходнике глиф — IMAGE_DATA(height, width, flags) и 1-битные строки. +Здесь его силуэт переводится в getimage (0xFF = прозрачность) и пакуется в +FONT\\FONT.ATL. Это именно шрифтовой атлас, а не заранее нарисованные +строки: номер ленты = код ASCII, поэтому runtime сохраняет произвольный +текст и пропорциональную ширину оригинала. + +Хвост страницы [0x3C00..0x3FFF] намеренно оставлен свободным: pop_menu.c +кладёт туда снимок игровой палитры на время паузы. Тогда 1 КБ не занимает +дефицитную W2. +""" +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SRC_SMALL = ROOT / "SDLPoP" / "src" / "menu.c" +SRC_BIG = ROOT / "SDLPoP" / "src" / "seg009.c" +OUT_DIR = ROOT / "poc" / "res" / "font" +OUT_ATL = OUT_DIR / "font.atl" +OUT_H = ROOT / "roomtest" / "pop_font.h" + +FIRST, LAST = 32, 126 +FONT_COLOR = 0x3F +# ВТОРОЙ ЦВЕТ КРУПНОГО ШРИФТА. У SDLPoP шрифт — маска, и цвет задаётся +# параметром рисования (show_hof_text рисует текст дважды: тень цветом 0 со +# сдвигом (1,1) и сам текст цветом 0xB7). У нас цвет запечён в пиксели +# глифа, поэтому второй цвет — это второй набор глифов. Он нужен таблице +# рекордов: чёрная тень строк и тёмное имя на золотой полосе ввода. +# Набор крупный, потому что draw_text() рисует hc_font (seg009:1594). +FONT_DARK_COLOR = 0x3E +# Каталог SPA1 хранит число картинок одним байтом, а три полных набора по +# 95 глифов — это 285. Тёмному набору полный ASCII и не нужен: им пишется +# только таблица рекордов (имена A..Z, время вида 59:43 и курсор ввода +# '_'), поэтому он обрезан по '_'. 190 + 64 = 254 картинки — потолок. +DARK_FIRST, DARK_LAST = 32, 95 +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 +SNAPSHOT_OFF = 0x3C00 +PAGE_SIZE = 16384 + + +def extract_array(path: Path, name: str) -> list[int]: + text = path.read_text() + match = re.search(rf"byte {name}\[\] = \{{(.*?)\n\}};", text, re.S) + assert match, f"{name} not found in {path}" + body = re.sub(r"//[^\n]*", "", match.group(1)) + body = re.sub(r"/\*.*?\*/", "", body, flags=re.S) + + def word(mm: re.Match) -> str: + value = int(mm.group(1), 0) + return f"{value & 0xFF}, {(value >> 8) & 0xFF}" + + def bin4(mm: re.Match) -> str: + b7, b6, b5, b4 = (int(v, 0) if v != "_" else 0 for v in mm.groups()) + return str((b4 << 4) | (b5 << 5) | (b6 << 6) | (b7 << 7)) + + def bin8(mm: re.Match) -> str: + bits = [int(v, 0) if v != "_" else 0 for v in mm.groups()] + return str(sum(bit << (7 - i) for i, bit in enumerate(bits))) + + def image_data(mm: re.Match) -> str: + h, w, flags = (int(v, 0) for v in mm.groups()) + return (f"{h & 0xFF}, {(h >> 8) & 0xFF}, " + f"{w & 0xFF}, {(w >> 8) & 0xFF}, " + f"{flags & 0xFF}, {(flags >> 8) & 0xFF}") + + body = re.sub(r"\bWORD\s*\(\s*(\w+)\s*\)", word, body) + body = re.sub(r"\bIMAGE_DATA\s*\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*\)", + image_data, body) + body = re.sub(r"\bBINARY_4\s*\(\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*" + r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*\)", bin4, body) + body = re.sub(r"\bBINARY_8\s*\(\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*" + r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*" + r"([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*,\s*([_\dxXa-fA-F]+)\s*\)", bin8, body) + body = re.sub(r"\b_\b", "0", body) + # Hex обязан идти первым: иначе regexp съедает в `0x20` только `0`. + return [int(v, 0) for v in re.findall(r"0[xX][0-9a-fA-F]+|\d+", body)] + + +def parse_glyphs(vals: list[int]) -> tuple[dict, list[dict]]: + first, last = vals[0], vals[1] + font = { + "first": first, + "last": last, + "ascent": vals[2] | (vals[3] << 8), + "below": vals[4] | (vals[5] << 8), + "space_lines": vals[6] | (vals[7] << 8), + "space_chars": vals[8] | (vals[9] << 8), + } + pos = 10 + (last - first + 1) * 2 # offsets у SDLPoP вычисляются в runtime + glyphs = [] + while pos < len(vals): + h = vals[pos] | (vals[pos + 1] << 8) + w = vals[pos + 2] | (vals[pos + 3] << 8) + pos += 6 # height, width, flags + stride = (w + 7) // 8 + rows = [] + for _ in range(h): + rows.append(vals[pos:pos + stride]) + pos += stride + glyphs.append({"h": h, "w": w, "rows": rows}) + assert len(glyphs) == last - first + 1, f"glyph count {len(glyphs)}" + return font, glyphs + + +def glyph_pixels(glyph: dict, color: int) -> bytes: + """1 бит/пиксель SDLPoP -> 8bpp getimage: 1 = color.""" + out = bytearray() + for row in glyph["rows"]: + for x in range(glyph["w"]): + byte = row[x // 8] + on = byte & (1 << (7 - (x & 7))) + out.append(color if on else 0xFF) + return bytes(out) + + +def pack_atlas(fonts: list[tuple[dict, list[dict], int]]) -> int: + entries = [] + for font, glyphs, color, first, last in fonts: + assert font["first"] <= first <= last <= font["last"] + entries.extend((glyph, color) for glyph in + glyphs[first - font["first"]:last - font["first"] + 1]) + assert len(entries) <= 255 + + data_off = ATL_DIR_OFF + len(entries) * 8 + out = bytearray(data_off) + out[0:4] = ATL_MAGIC + out[4] = len(entries) + for index, (glyph, color) in enumerate(entries): + offset = len(out) + pixels = glyph_pixels(glyph, color) + out += glyph["w"].to_bytes(2, "little") + out += glyph["h"].to_bytes(2, "little") + out += pixels + directory = ATL_DIR_OFF + index * 8 + out[directory:directory + 2] = offset.to_bytes(2, "little") + out[directory + 2] = glyph["w"] + out[directory + 3] = glyph["h"] + out[directory + 4] = 1 + out[directory + 5] = 1 + + if len(out) > SNAPSHOT_OFF: + raise SystemExit(f"font atlas: {len(out)} Б, нет места для palette snapshot @ {SNAPSHOT_OFF:#x}") + OUT_DIR.mkdir(parents=True, exist_ok=True) + OUT_ATL.write_bytes(out) + return len(out) + + +def emit_header(small: dict, big: dict, atlas_size: int) -> None: + small_n = LAST - FIRST + 1 + text = f"""/* + * pop_font.h — контракт FONT\\FONT.ATL, СГЕНЕРИРОВАН. + * Источник: SDLPoP hc_small_font_data[] и hc_font_data[]. + * Генератор: toolchain/pop_extract_font.py. Руками не править. + */ +#ifndef POP_FONT_H +#define POP_FONT_H + +#include + +#define POP_FONT_COLOR 0x{FONT_COLOR:02X} +#define POP_FONT_DARK_COLOR 0x{FONT_DARK_COLOR:02X} +#define POP_FONT_FIRST {FIRST} +#define POP_FONT_LAST {LAST} +#define POP_FONT_SMALL_BASE 0 +#define POP_FONT_SMALL_ASCENT {small['ascent']} +#define POP_FONT_SMALL_SPACE {small['space_chars']} +#define POP_FONT_BIG_BASE {small_n} +#define POP_FONT_BIG_ASCENT {big['ascent']} +#define POP_FONT_BIG_SPACE {big['space_chars']} +/* Тот же КРУПНЫЙ шрифт цветом POP_FONT_DARK_COLOR: тень и текст на светлом. + * Набор обрезан по '_' — им пишется только таблица рекордов. */ +#define POP_FONT_DARK_BASE {2 * small_n} +#define POP_FONT_DARK_FIRST {DARK_FIRST} +#define POP_FONT_DARK_LAST {DARK_LAST} +#define POP_FONT_SNAPSHOT_OFF 0x{SNAPSHOT_OFF:04X} +#define POP_FONT_ATLAS_SIZE {atlas_size} + +#endif +""" + OUT_H.write_text(text) + + +def main() -> None: + parsed = [] + for path, name in ((SRC_SMALL, "hc_small_font_data"), (SRC_BIG, "hc_font_data")): + font, glyphs = parse_glyphs(extract_array(path, name)) + print(f"{name}: chars={font['first']}..{font['last']} ascent={font['ascent']} " + f"space={font['space_chars']}") + parsed.append((font, glyphs)) + sets = [(parsed[0][0], parsed[0][1], FONT_COLOR, FIRST, LAST), + (parsed[1][0], parsed[1][1], FONT_COLOR, FIRST, LAST), + (parsed[1][0], parsed[1][1], FONT_DARK_COLOR, DARK_FIRST, DARK_LAST)] + atlas_size = pack_atlas(sets) + emit_header(parsed[0][0], parsed[1][0], atlas_size) + print(f"{OUT_ATL}: 2 x {LAST - FIRST + 1} + {DARK_LAST - DARK_FIRST + 1} glyphs, " + f"{atlas_size} Б; " + f"snapshot from 0x{SNAPSHOT_OFF:04X}") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/pop_pack_intro.py b/applications/PoP/toolchain/pop_pack_intro.py new file mode 100644 index 0000000..d529d77 --- /dev/null +++ b/applications/PoP/toolchain/pop_pack_intro.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +"""Упаковка story/PV intro SDLPoP в полосы SPA1 для Sprinter. + +Исходники остаются единственным источником истины: TITLE/res41..45, res51 и +PV/res800/850/950/980. Runtime получает только готовые 320x200 композиции +и 256-цветные палитры. Это намеренно переносит смешивание палитров Princess, +Jaffar, спальни и кровати из ограниченного Z80 в build-time. +""" +from pathlib import Path + +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +TITLE = ROOT / "SDLPoP" / "data" / "TITLE" +PV = ROOT / "SDLPoP" / "data" / "PV" +PRINCE = ROOT / "SDLPoP" / "data" / "PRINCE" +KID = ROOT / "SDLPoP" / "data" / "KID" +OUT = ROOT / "poc" / "res" / "pv" + +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 +ATL_MIN_SIZE = 0x100 +PAGE_SIZE = 16384 +STRIP_H = 49 + +# ФОН ТЕКСТОВОЙ РАМКИ — ОТДЕЛЬНЫЙ ИНДЕКС. У оригинала это индекс 14 в +# строке палитры спрайтов title40, и load_title_images() красит его либо в +# #100060 (интро), либо в #800000 (финал, seg001:586). Раньше мы +# ремапили 14 в 9 — «тёмно-синий» палитры res51; подменить его нельзя, +# потому что тем же индексом нарисована сама титульная картинка (8265 +# пикселей). Поэтому фон уезжает в свободный индекс, и финал красит +# только его (pop_pal_story_ending). +STORY_BG_INDEX = 16 +STORY_BG_COLOR = (0x10, 0x00, 0x60) +# Цвет текста таблицы рекордов: 0xB7 оригинала = седьмой цвет палитры +# title (224,160,0). Глифы шрифта запечены индексом POP_FONT_COLOR, так +# что цвет им даёт именно эта запись; тёмный набор (POP_FONT_DARK_COLOR) +# остаётся чёрным — старшие записи story.pal и так чёрные. +STORY_FONT_INDEX = 0x3F + + +def transpose_cols(width: int, height: int, rowmajor: bytes) -> bytes: + """Row-major -> column-major для бесплатного horizontal flip в BGI.""" + out = bytearray(width * height) + for column in range(width): + base = column * height + for row in range(height): + out[base + row] = rowmajor[row * width + column] + return bytes(out) + + +def atlas_blob(pixels: bytes, width: int, height: int) -> bytes: + """Один непрозрачный getimage в минимальном SPA1-атласе.""" + data_off = ATL_DIR_OFF + 8 + out = bytearray(data_off) + out[:4] = ATL_MAGIC + out[4] = 1 + out[ATL_DIR_OFF:ATL_DIR_OFF + 2] = data_off.to_bytes(2, "little") + out[ATL_DIR_OFF + 2] = min(width, 255) + out[ATL_DIR_OFF + 3] = height + out[ATL_DIR_OFF + 4] = out[ATL_DIR_OFF + 5] = 1 + out += width.to_bytes(2, "little") + height.to_bytes(2, "little") + out += pixels + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) + if len(out) > PAGE_SIZE: + raise SystemExit(f"intro atlas {len(out)} B > {PAGE_SIZE} B") + return bytes(out) + + +def sprite_atlas(path: Path, entries: dict[int, tuple[int, int, bytes]]) -> None: + """Атлас небольших прозрачных кадров, целиком живущий в одной EMM. + + В отличие от atlas_blob() для полноэкранной полосы, здесь нужен каталог + нескольких картинок: runtime держит страницу открытой всю PV-сцену и + переключает только idx. Пустые индексы не требуются — все наборы ниже + нумеруются подряд с нуля. + """ + count = len(entries) + if not count or count > 255: + raise SystemExit(f"{path}: bad sprite atlas count {count}") + data_off = ATL_DIR_OFF + count * 8 + directory = bytearray(count * 8) + blobs = bytearray() + for idx in range(count): + width, height, pixels = entries[idx] + offset = data_off + len(blobs) + blobs += width.to_bytes(2, "little") + height.to_bytes(2, "little") + blobs += pixels + d = idx * 8 + directory[d:d + 2] = offset.to_bytes(2, "little") + directory[d + 2] = width + directory[d + 3] = height + directory[d + 4] = directory[d + 5] = 1 + out = bytearray(data_off) + out[:4] = ATL_MAGIC + out[4] = count + out[ATL_DIR_OFF:ATL_DIR_OFF + len(directory)] = directory + out += blobs + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) + if len(out) > PAGE_SIZE: + raise SystemExit(f"{path}: {len(out)} B > one EMM page") + path.write_bytes(out) + + +def sprite_frames(directory: Path, first: int, last: int, + color_base: int, column_major: bool = False + ) -> dict[int, tuple[int, int, bytes]]: + """PNG-кадры -> getimage. Ноль PNG становится прозрачным 0xFF BGI.""" + result = {} + for index, number in enumerate(range(first, last + 1)): + image = png(directory, number) + pixels = bytes(0xFF if pixel == 0 else color_base + pixel + for pixel in image.tobytes()) + if column_major: + pixels = transpose_cols(image.width, image.height, pixels) + result[index] = image.width, image.height, pixels + return result + + +def png(directory: Path, number: int) -> Image.Image: + image = Image.open(directory / f"res{number}.png") + if image.mode != "P": + raise SystemExit(f"res{number}.png: expected indexed PNG, got {image.mode}") + return image.copy() + + +def pal_file(path: Path) -> list[tuple[int, int, int]]: + """16 цветов из .pal ресурса (4 байта заголовка, дальше RGB по 6 бит).""" + raw = path.read_bytes() + return [(raw[4 + i * 3] << 2, raw[5 + i * 3] << 2, raw[6 + i * 3] << 2) + for i in range(16)] + + +def palette(image: Image.Image) -> list[tuple[int, int, int]]: + raw = image.getpalette() + return [tuple(raw[index * 3:index * 3 + 3]) for index in range(16)] + + +def write_palette(path: Path, entries: list[tuple[int, int, int]], + extra=None) -> None: + """B,G,R,0 — ровно формат gfx_pal_fload; незадействованное чёрное.""" + out = bytearray() + entries = (entries + [(0, 0, 0)] * 256)[:256] + for index, color in (extra or {}).items(): + entries[index] = color + for red, green, blue in entries: + out += bytes((blue, green, red, 0)) + path.write_bytes(out) + + +def write_scene(prefix: str, number: int, canvas: Image.Image) -> None: + if canvas.size != (320, 200): + raise SystemExit(f"{prefix}{number}: expected 320x200, got {canvas.size}") + for part, top in enumerate(range(0, 200, STRIP_H)): + band = canvas.crop((0, top, 320, min(top + STRIP_H, 200))) + (OUT / f"{prefix}{number}_{part}.atl").write_bytes( + atlas_blob(band.tobytes(), band.width, band.height)) + + +def story_scenes() -> list[Image.Image]: + """show_title/end_sequence в единой с title палитре. + + res41 кодирует тёмный фон индексом 14, а его палитра делает этот индекс + чёрным; show_title() SDLPoP подменяет его #100060 либо #800000. Тексты + res42..45 имеют индекс 1, но этот индекс у res41 тоже тёмный; ремап на + индекс title/res51 15 (белый) позволяет проявлять историю прямо поверх + последнего title-экрана без смены палитры. Фон уезжает в + STORY_BG_INDEX, чтобы финал мог покрасить его отдельно. + + Последний экран — фон таблицы рекордов: та же рамка плюс логотип + PRINCE OF PERSIA на y=24 (HOF_POP оригинала — тот же спрайт res54, что + и в титрах, только выше). + """ + frame_raw = bytearray(png(TITLE, 41).tobytes()) + for offset, pixel in enumerate(frame_raw): + if pixel == 14: + frame_raw[offset] = STORY_BG_INDEX + frame = Image.frombytes("P", (320, 200), bytes(frame_raw)) + result = [] + for text_number, ypos in ((42, 25), (43, 25), (45, 26), (44, 25)): + canvas = frame.copy() + raw = bytearray(canvas.tobytes()) + text = png(TITLE, text_number) + for sy in range(text.height): + for sx in range(text.width): + if text.getpixel((sx, sy)): + raw[(ypos + sy) * 320 + 24 + sx] = 15 + canvas.frombytes(bytes(raw)) + result.append(canvas) + result.append(frame) + + hof = frame.copy() + raw = bytearray(hof.tobytes()) + logo = png(TITLE, 54) + for sy in range(logo.height): + for sx in range(logo.width): + pixel = logo.getpixel((sx, sy)) + if pixel: # blitters_10h_transp: 0 сквозной + raw[(24 + sy) * 320 + 24 + sx] = pixel + hof.frombytes(bytes(raw)) + result.append(hof) + return result + + +def pv_palette_canvas() -> tuple[Image.Image, list[tuple[int, int, int]]]: + """Спальня + bed. Номера 0..15=room, 16..31=Princess, + 32..47=Jaffar, 48..63=bed. Благодаря этому у смешанных PNG не теряются + исходные цвета при одном gfx_pal_fload() на весь PV.""" + room = png(PV, 951) + bed = png(PV, 981) + raw = bytearray(room.tobytes()) + bed_raw = bed.tobytes() + for sy in range(bed.height): + for sx in range(bed.width): + pixel = bed_raw[sy * bed.width + sx] + if pixel: + raw[(142 + sy) * 320 + sx] = 48 + pixel + canvas = Image.frombytes("P", room.size, bytes(raw)) + # 80..95 — пламя факелов. Это отдельный chtab PRINCE в оригинале; без + # отдельной строки цвета динамические кадры становились бы цветами стены. + # 112..127 (0x70) — палитра КИДА: на PV-экране финала и сцен 8/9 он и + # мышь рисуются СВОИМИ атласами (chtab_2), а их индексы запечены под + # игровые слоты 0x70. Без этой строки они выходили бы чёрными. + return (canvas, palette(room) + palette(png(PV, 801)) + + palette(png(PV, 851)) + palette(bed) + palette(png(PV, 901)) + + palette(png(PRINCE, 151)) + [(0, 0, 0)] * 16 + + pal_file(KID / "res400.pal")) + + +def sprite_paste(dst: Image.Image, sprite: Image.Image, x: int, floor: int, + color_base: int) -> None: + """Положить спрайт ногами на floor, remap его палитру в отдельную строку.""" + raw = bytearray(dst.tobytes()) + src = sprite.tobytes() + y = floor - sprite.height + for sy in range(sprite.height): + dy = y + sy + if dy < 0 or dy >= 200: + continue + for sx in range(sprite.width): + dx = x + sx + pixel = src[sy * sprite.width + sx] + if pixel and 0 <= dx < 320: + raw[dy * 320 + dx] = color_base + pixel + dst.frombytes(bytes(raw)) + + +def pv_scenes() -> tuple[list[Image.Image], list[tuple[int, int, int]]]: + base, colors = pv_palette_canvas() + + # Позиции Char из init_princess()/init_vizier(): x=120/198, y=166. + # В оригинале точный offset задаёт frame table; статические композиции + # выравнивают персонажей по полу y=166, не хранят таблицы PV-анимации. + waiting = base.copy() + sprite_paste(waiting, png(PV, 801), 110, 166, 16) + + jaffar = waiting.copy() + sprite_paste(jaffar, png(PV, 851), 188, 166, 32) + + magic = base.copy() + sprite_paste(magic, png(PV, 805), 110, 166, 16) + sprite_paste(magic, png(PV, 870), 188, 166, 32) + # res953 — первый полный кадр песочных часов; он использует палитру room. + sprite_paste(magic, png(PV, 953), 18, 166, 0) + + alone = base.copy() + sprite_paste(alone, png(PV, 816), 110, 166, 16) + sprite_paste(alone, png(PV, 959), 18, 166, 0) + + # FG9: статические ключевые позы тех же scene 2/4/6/8/9/12. PV2 + # использует второй набор Princess (res900), поэтому он лежит в строке + # палитры 64..79. Мышь в оригинале меняет только короткую позу рядом с + # Princess; здесь соответствующая стадия выделена отдельным кадром. + stand = base.copy() + sprite_paste(stand, png(PV, 801), 134, 166, 16) + + lying = base.copy() + sprite_paste(lying, png(PV, 901), 78, 162, 64) + # Часы для cutscene_4 выбираются runtime по оставшимся минутам и + # анимируются вместе с песком; в статической основе их быть не должно. + + mouse = base.copy() + sprite_paste(mouse, png(PV, 920), 120, 169, 64) + sprite_paste(mouse, png(PV, 959), 18, 166, 0) + + short_time = base.copy() + sprite_paste(short_time, png(PV, 816), 146, 166, 16) + sprite_paste(short_time, png(PV, 953), 18, 166, 0) + + return [waiting, jaffar, magic, alone, stand, lying, mouse, short_time], colors + + +def pv_animation_assets() -> None: + """Ресурсы покадрового intro. + + `b0_*` — единственный чистый фон. Он копируется accelerator-ом между + обеими экранными страницами; спрайты рисуются банком без shadow, так что + следующий кадр автоматически начинает с фона. Каждый actor-атлас <=16K, + поэтому нет чтения 320x200 с HDD в анимационном цикле. + """ + base, _ = pv_palette_canvas() + write_scene("b", 0, base) + # frame_tbl_cuts из SDLPoP адресует Princess как image 0..16 ресурса + # 800 (то есть PNG res801..817). Для Jaffar PV1 адресуется ресурс 850: + # image 0..37 = res851..888. Все 38 кадров не помещаются в одну EMM, + # поэтому сохраняем их тремя последовательными атласами. Runtime знает + # границы, но номер кадра остаётся исходным image-id 0..37. + sprite_atlas(OUT / "a0.atl", sprite_frames(PV, 801, 817, 16, True)) + sprite_atlas(OUT / "j0.atl", sprite_frames(PV, 851, 863, 32, True)) + sprite_atlas(OUT / "j1.atl", sprite_frames(PV, 864, 874, 32, True)) + sprite_atlas(OUT / "j2.atl", sprite_frames(PV, 875, 888, 32, True)) + # ВТОРОЙ набор Princess (PV2, res901..930 = image 0..29 ресурса 900). + # Он нужен сценам 8/9 (сидит и гладит мышь, встаёт, приседает) и финалу + # (поворачивается и обнимает Кида) — поз PV1 для этого не хватает. + # 26.7 КБ не влезают в одну EMM, поэтому два атласа; номер кадра при + # этом остаётся исходным image-id 0..29, границу знает runtime. + # Палитровая строка 64..79 — та же, что у PV2 в статических сценах. + sprite_atlas(OUT / "a1.atl", sprite_frames(PV, 901, 917, 64, True)) + sprite_atlas(OUT / "a2.atl", sprite_frames(PV, 918, 930, 64, True)) + # res953/res954 — состояния часов, res960..962 — три фазы песка. + sprite_atlas(OUT / "h0.atl", sprite_frames(PV, 952, 962, 0)) + sprite_atlas(OUT / "t0.atl", sprite_frames(PRINCE, 151, 159, 80)) + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + + stories = story_scenes() + write_palette(OUT / "story.pal", palette(png(TITLE, 51)), + {STORY_BG_INDEX: STORY_BG_COLOR, + STORY_FONT_INDEX: (224, 160, 0)}) + for number, scene in enumerate(stories): + write_scene("s", number, scene) + + scenes, colors = pv_scenes() + write_palette(OUT / "pv.pal", colors) + for number, scene in enumerate(scenes): + write_scene("p", number, scene) + + pv_animation_assets() + + print(f"{OUT}: {len(stories)} story + 8 PV compositions + base x 5 strips, " + f"6 animated actor atlases, one strip/page <= {STRIP_H} rows") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/pop_pack_kid.py b/applications/PoP/toolchain/pop_pack_kid.py index 60a9596..d8f1412 100644 --- a/applications/PoP/toolchain/pop_pack_kid.py +++ b/applications/PoP/toolchain/pop_pack_kid.py @@ -21,7 +21,7 @@ HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import pop_pack_bg as B # pack_atlas, blob, scale6to8, ENV_SHIFT -KID_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/KID" +KID_DIR = os.path.join(HERE, "..", "SDLPoP", "data", "KID") OUT_DIR = os.path.join(HERE, "..", "poc", "res", "kid") PAL_BASE = 0x70 # слоты палитры Sprinter под Kid SWORD_PAL_BASE = 0x80 # chtab_0 (меч в руке) -> слоты 0x80..0x8F diff --git a/applications/PoP/toolchain/pop_pack_title.py b/applications/PoP/toolchain/pop_pack_title.py new file mode 100644 index 0000000..0f739d9 --- /dev/null +++ b/applications/PoP/toolchain/pop_pack_title.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""pop_pack_title.py — титульные композиции SDLPoP для Sprinter. + +Исходник — PNG-ресурсы ``data/TITLE/res51..55`` из локального SDLPoP. +Runtime не должен держать 320x200 картинку в W2 и не разбирает PNG: каждая +готовая композиция разрезана на пять getimage-лент высотой не более 49 +строк. Лента вместе с заголовком атласа занимает одну EMM-страницу; экран +рисуется лента за лентой, и страница сразу освобождается. + +Композиции в точности повторяют ``show_title()`` до story/intro: + t0 — основной рисунок; + t1 — рисунок + PRESENTS; + t2 — рисунок + название игры; + t3 — рисунок + логотип PRINCE OF PERSIA и Jordan Mechner. +""" +from pathlib import Path + +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "SDLPoP" / "data" / "TITLE" +OUT = ROOT / "poc" / "res" / "title" + +ATL_MAGIC = b"SPA1" +ATL_DIR_OFF = 0x68 +ATL_MIN_SIZE = 0x100 +PAGE_SIZE = 16384 +STRIP_H = 49 + + +def atlas_blob(pixels: bytes, width: int, height: int) -> bytes: + """Один непрозрачный getimage в минимальном SPA1-атласе.""" + data_off = ATL_DIR_OFF + 8 + out = bytearray(data_off) + out[:4] = ATL_MAGIC + out[4] = 1 + out[ATL_DIR_OFF:ATL_DIR_OFF + 2] = data_off.to_bytes(2, "little") + # Каталог хранит preview-габарит в uint8, но atlas_image читает только + # offset; реальный getimage-width — u16 в блобе. Так уже пакуются + # широкие 320-пиксельные полосы фона. + out[ATL_DIR_OFF + 2] = min(width, 255) + out[ATL_DIR_OFF + 3] = height + out[ATL_DIR_OFF + 4] = out[ATL_DIR_OFF + 5] = 1 + out += width.to_bytes(2, "little") + height.to_bytes(2, "little") + out += pixels + if len(out) < ATL_MIN_SIZE: + out += bytes(ATL_MIN_SIZE - len(out)) + if len(out) > PAGE_SIZE: + raise SystemExit(f"title atlas {len(out)} B > {PAGE_SIZE} B") + return bytes(out) + + +def image(n: int) -> Image.Image: + im = Image.open(SRC / f"res{n}.png") + if im.mode != "P": + raise SystemExit(f"res{n}.png: expected indexed PNG, got {im.mode}") + return im.copy() + + +def assert_palette(base: Image.Image, overlay: Image.Image, n: int) -> None: + if base.getpalette()[:48] != overlay.getpalette()[:48]: + raise SystemExit(f"res{n}.png uses a palette different from res51.png") + + +def paste_opaque(dst: Image.Image, src: Image.Image, x: int, y: int, n: int) -> None: + assert_palette(dst, src, n) + dst.paste(src, (x, y)) + + +def paste_transparent(dst: Image.Image, src: Image.Image, x: int, y: int, n: int) -> None: + assert_palette(dst, src, n) + # TITLE_POP — единственный из пяти элементов, который SDLPoP рисует + # blitters_10h_transp. В PNG тот же факт записан colour-key index 0. + # Image.point() у P-изображения возвращает снова P, а paste() принимает + # маску только L/1/RGBA. Строим L явно, не меняя индексы самой картинки. + mask = Image.frombytes("L", src.size, + bytes(0 if p == 0 else 255 for p in src.tobytes())) + dst.paste(src, (x, y), mask) + + +# Семнадцатая запись — фон текстовой рамки story (STORY_BG_INDEX в +# pop_pack_intro.py). Она нужна здесь потому, что «In the absence…» +# проявляется полосами ПОВЕРХ последнего кадра титров, то есть при ещё +# загруженной title.pal: без этой записи фон рамки был бы чёрным. +STORY_BG_INDEX = 16 +STORY_BG_COLOR = (0x10, 0x00, 0x60) + + +def write_palette(base: Image.Image) -> None: + rgb = base.getpalette()[:48] + pal = bytearray() + for i in range(16): + r, g, b = rgb[i * 3:i * 3 + 3] + pal += bytes((b, g, r, 0)) # родной формат gfx_pal_fload + assert len(pal) == STORY_BG_INDEX * 4 + r, g, b = STORY_BG_COLOR + pal += bytes((b, g, r, 0)) + (OUT / "title.pal").write_bytes(pal) + + +def write_scene(scene: int, canvas: Image.Image) -> int: + count = 0 + for part, top in enumerate(range(0, canvas.height, STRIP_H)): + band = canvas.crop((0, top, canvas.width, + min(top + STRIP_H, canvas.height))) + path = OUT / f"t{scene}_{part}.atl" + path.write_bytes(atlas_blob(band.tobytes(), band.width, band.height)) + count += 1 + return count + + +def main() -> None: + OUT.mkdir(parents=True, exist_ok=True) + main_image = image(51) + if main_image.size != (320, 200): + raise SystemExit(f"res51.png: expected 320x200, got {main_image.size}") + + presents = main_image.copy() + paste_opaque(presents, image(52), 96, 106, 52) + + game = main_image.copy() + paste_opaque(game, image(53), 96, 122, 53) + + logo = main_image.copy() + paste_transparent(logo, image(54), 24, 107, 54) + paste_opaque(logo, image(55), 48, 184, 55) + + write_palette(main_image) + scenes = (main_image, presents, game, logo) + strips = [write_scene(i, scene) for i, scene in enumerate(scenes)] + assert strips == [5, 5, 5, 5] + print(f"{OUT}: {len(scenes)} title compositions x {strips[0]} strips, " + f"one strip <= {STRIP_H} rows / one EMM page") + + +if __name__ == "__main__": + main() diff --git a/applications/PoP/toolchain/render_room.py b/applications/PoP/toolchain/render_room.py index 87a41fb..b441515 100755 --- a/applications/PoP/toolchain/render_room.py +++ b/applications/PoP/toolchain/render_room.py @@ -30,9 +30,10 @@ animation) for this first static render; they use curr_modifier/backtable. import os from PIL import Image -LEVEL_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/LEVELS" -VPALACE_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VPALACE" -VDUNGEON_DIR = "/Users/alex/Projects/DIY/Z80/Sprinter/C-Compiler/applications/PoP/SDLPoP/data/VDUNGEON" +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LEVEL_DIR = os.path.join(ROOT, "SDLPoP", "data", "LEVELS") +VPALACE_DIR = os.path.join(ROOT, "SDLPoP", "data", "VPALACE") +VDUNGEON_DIR = os.path.join(ROOT, "SDLPoP", "data", "VDUNGEON") # For dungeon levels (tbl_level_type == 0) the environment sprites come from # VDUNGEON where present, otherwise fall back to the shared VPALACE set diff --git a/applications/SprPoP/.gitignore b/applications/SprPoP/.gitignore index 08b9da9..fe4480c 100644 --- a/applications/SprPoP/.gitignore +++ b/applications/SprPoP/.gitignore @@ -62,6 +62,10 @@ build/ # --------------------------------------------------------------------------- # ОС / редакторы / локальные настройки # --------------------------------------------------------------------------- +local.mk +.resource-stamps/ +__pycache__/ +*.py[cod] .DS_Store ._* *~ diff --git a/applications/SprPoP/LICENSE b/applications/SprPoP/LICENSE new file mode 100644 index 0000000..a738358 --- /dev/null +++ b/applications/SprPoP/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sprinter C Compiler contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/SprPoP/Makefile b/applications/SprPoP/Makefile index 6b23b2f..14abc9b 100644 --- a/applications/SprPoP/Makefile +++ b/applications/SprPoP/Makefile @@ -22,14 +22,20 @@ # Основные цели: # make собрать sprpop.exe и разложить build/ # make hdd + образ build/hdd/sprpop.chd -# make mame-link однократно связать образ с MAME (см. ниже) +# make run-hdd запустить MAME с локальным образом игры # make resources перегенерировать ресурсы из assets/orig/ # make music-mp3 музыка из другого набора записей (flac|mp3|ogg|mt32) # make clean снести build/ (ассеты не трогает) # make distclean clean + снести упакованные ассеты assets/packed/ -SPRINTER_ROOT ?= $(abspath $(CURDIR)/../..) +-include $(CURDIR)/local.mk +SPRINTER_ROOT ?= $(if $(wildcard $(CURDIR)/../../bin/sprinter-cc),$(abspath $(CURDIR)/../..),) +ifeq ($(strip $(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT или создайте SprPoP/local.mk) +endif PROJ_ROOT := $(SPRINTER_ROOT) +export SPRINTER_ROOT MAME_HOME MAME_BIN MAME_ROMPATH MAME_DSS_IMAGE +export MAME_SYSTEM_HDD_IMAGE MAME_BIOS CHDMAN_BIN EXAMPLE := sprpop SRC_DIR := src BUILD_DIR := build @@ -259,8 +265,7 @@ HDD_PACK_ARGS := $(BUILD_DIR)/$(EXAMPLE).exe \ # Всё дерево SprPoP лежит рядом с EXE, но не меняет дефолт глобального # упаковщика: остальные приложения по-прежнему попадают в корень HDD. HDD_DEST_DIR := games/sprpop -# Образ живёт в build/, а не в mame/v306/IMG — приложение автономно. Связь с -# MAME — одна символьная ссылка, ставится однократно: make mame-link. +# Образ живёт в build/; run-hdd передаёт его MAME напрямую как hard2. HDD_IMG := $(CURDIR)/$(BUILD_DIR)/hdd/$(EXAMPLE).chd include $(PROJ_ROOT)/app.mk @@ -560,14 +565,6 @@ hdd: stage | $(BUILD_DIR)/hdd $(BUILD_DIR)/hdd: @mkdir -p $@ -# Однократная связка с MAME: run_bridge.sh жёстко открывает IMG/test_hdd.chd, -# поэтому подсовываем ему символьную ссылку на наш образ. chdman переписывает -# сам файл, ссылка остаётся живой — но MAME после пересборки образа ОБЯЗАН -# полный рестарт (новый inode). -mame-link: hdd - ln -sf $(HDD_IMG) $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd - @echo "MAME: IMG/test_hdd.chd -> $(HDD_IMG)" - # Музыка из конкретного набора записей. Каждая цель пересобирает музыку и # сразу раскладывает её в build/, чтобы `make hdd` взял уже новую. # make music-mp3 собрать музыку из mp3 @@ -624,4 +621,4 @@ distclean: clean .PHONY: stage check-orig resources resources-rebuild resources-bg resources-kid \ resources-actors resources-sound resources-music resources-font \ resources-title resources-pv resources-levels $(MUSIC_TARGETS) \ - $(FETCH_TARGETS) test-tools relink distclean mame-link + $(FETCH_TARGETS) test-tools relink distclean run-hdd diff --git a/applications/SprPoP/README.md b/applications/SprPoP/README.md index 7e44cf6..33f9859 100644 --- a/applications/SprPoP/README.md +++ b/applications/SprPoP/README.md @@ -8,9 +8,10 @@ сборка лежат внутри этой папки. Наружу нужен только компилятор: ```sh -export SPRINTER_ROOT=/путь/к/C-Compiler # не нужен, если папка лежит внутри его дерева +export SPRINTER_ROOT=/путь/к/C-Compiler make # build/sprpop.exe + ресурсы в build/ make hdd # + образ build/hdd/sprpop.chd +make run-hdd MAME_HOME=/путь/к/MAME/runtime # запуск с локальным CHD ``` Чужих данных в репозитории нет — есть адреса, откуда их взять. Чтобы @@ -39,7 +40,7 @@ build/ выход: sprpop.exe, каталоги ресурсов, hdd/, |---|---| | `make` | собрать `build/sprpop.exe` и разложить ресурсы в `build/` | | `make hdd` | + образ жёсткого диска `build/hdd/sprpop.chd` | -| `make mame-link` | однократно подставить образ в MAME (символьная ссылка на `IMG/test_hdd.chd`) | +| `make run-hdd` | передать локальный CHD в MAME как `-hard2` без изменения установки эмулятора | | `make fetch` | скачать внешние исходные данные в `assets/orig/`: `fetch-sdlpop`, `fetch-music`, `fetch-check`, `fetch-list` | | `make resources` | перегенерировать ресурсы из `assets/orig/` | | `make music-mp3` | музыка из другого набора записей: `music-flac` (умолчание), `music-mp3`, `music-ogg`, `music-mt32` | diff --git a/applications/SprPoP/tests/host/Makefile b/applications/SprPoP/tests/host/Makefile index f407541..de521a4 100644 --- a/applications/SprPoP/tests/host/Makefile +++ b/applications/SprPoP/tests/host/Makefile @@ -2,7 +2,11 @@ # здесь — только сами наборы и список модулей, которые в них линкуются. # Наружу тесты знают тот же единственный путь, что и само приложение, — # корень тулчейна (см. ../../Makefile). Всё остальное считается от него. -SPRINTER_ROOT ?= $(abspath $(CURDIR)/../../../..) +-include $(CURDIR)/../../local.mk +SPRINTER_ROOT ?= $(if $(wildcard $(CURDIR)/../../../../bin/sprinter-cc),$(abspath $(CURDIR)/../../../..),) +ifeq ($(strip $(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT для host-тестов SprPoP) +endif TESTKIT := $(SPRINTER_ROOT)/testkit APP_DIR := $(abspath $(CURDIR)/../..) ENGINE_DIR := $(APP_DIR)/src diff --git a/applications/Volkov/.gitignore b/applications/Volkov/.gitignore index b307c25..1ff0398 100644 --- a/applications/Volkov/.gitignore +++ b/applications/Volkov/.gitignore @@ -6,3 +6,18 @@ # Клон исторических исходников VC служит локальным справочным материалом и # сохраняет собственный .git; во внешний репозиторий его как gitlink не кладём. /VolkovCommander/ + +# Собственный выход Commander и локальные пути к SDK/MAME. +local.mk +build/ +artifacts/*-local/ +.sprinter-cc-*/ +.resource-stamps/ +*.o +*.obj +*.dSYM/ +__pycache__/ +*.py[cod] +.DS_Store +._* +.vscode/ diff --git a/applications/Volkov/.python-version b/applications/Volkov/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/applications/Volkov/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/applications/Volkov/LICENSE b/applications/Volkov/LICENSE new file mode 100644 index 0000000..a738358 --- /dev/null +++ b/applications/Volkov/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sprinter C Compiler contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/applications/Volkov/Makefile b/applications/Volkov/Makefile index fde9978..fe4263d 100644 --- a/applications/Volkov/Makefile +++ b/applications/Volkov/Makefile @@ -1,13 +1,12 @@ # Sprinter Commander — двухпанельный файловый менеджер. -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := sprcmd SRC_DIR := src BUILD_DIR := build MEMORY := big ALLOCS ?= 3000 HDD_IMG := $(CURDIR)/build/hdd/sprcmd.chd -MDVIEW2_DIR := $(PROJ_ROOT)/examples/mdview2 +MDVIEW2_DIR := $(CURDIR)/tests/fixtures/mdview2 MDVIEW2_SRCS := $(wildcard $(MDVIEW2_DIR)/*.c) MDVIEW2_HEADERS := $(wildcard $(MDVIEW2_DIR)/*.h) MDVIEW2_TEST_EXE := $(CURDIR)/build/external/mdview2.exe @@ -38,7 +37,7 @@ EXTRA_SRCS := \ src/sc_theme.c \ src/sc_video_system.c -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/sdk.mk HEADERS := $(wildcard include/*.h) diff --git a/applications/Volkov/sdk.mk b/applications/Volkov/sdk.mk new file mode 100644 index 0000000..b0d0882 --- /dev/null +++ b/applications/Volkov/sdk.mk @@ -0,0 +1,11 @@ +# Общая привязка Commander и его target-проб к Sprinter-CC. +VOLKOV_ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +-include $(VOLKOV_ROOT)/local.mk +SPRINTER_ROOT ?= $(if $(wildcard $(VOLKOV_ROOT)/../../bin/sprinter-cc),$(abspath $(VOLKOV_ROOT)/../..),) +ifeq ($(strip $(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT или создайте Volkov/local.mk) +endif +PROJ_ROOT := $(SPRINTER_ROOT) +export SPRINTER_ROOT MAME_HOME MAME_BIN MAME_ROMPATH MAME_DSS_IMAGE +export MAME_SYSTEM_HDD_IMAGE MAME_BIOS CHDMAN_BIN +include $(PROJ_ROOT)/app.mk diff --git a/applications/Volkov/tests/fixtures/mdview2/LICENSE b/applications/Volkov/tests/fixtures/mdview2/LICENSE new file mode 100644 index 0000000..217ff75 --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/LICENSE @@ -0,0 +1,41 @@ +MIT License + +Copyright (c) 2026 Sprinter C Compiler contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ + +Vendored third-party components are under their own licenses: + + third_party/sdcc/ — SDCC 4.5 under GPL v2 with linking exception. + See third_party/sdcc/COPYING.txt. + third_party/solid-c/ — Original Sprinter Solid C, used only as a reference + for compatibility. Licence not specified upstream; + treat as reference material, do not redistribute + original binaries. + mame/v306/ — MAME (https://www.mamedev.org), GPL v2+. + mame/v306/IMG/*.img *.chd *.iso — Sprinter ROM / DSS / sample disk images + from Peters Plus. Redistribution policy: see + Peters Plus documentation. + +Documentation in docs/converted/, docs/reference/, docs/samples/, and +docs/memory management/ contains material originally published by Peters Plus +team (Иван Мак, Дмитрий Паринов and others). Used here for cross-compilation +reference; original copyrights apply. diff --git a/applications/Volkov/tests/fixtures/mdview2/ORIGIN.md b/applications/Volkov/tests/fixtures/mdview2/ORIGIN.md new file mode 100644 index 0000000..e16bcf3 --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/ORIGIN.md @@ -0,0 +1,8 @@ +# MDVIEW2 fixture для теста запуска дочернего приложения + +Снимок исходников `examples/mdview2` из Sprinter-SDCC commit `5323b16` +(2026-09-15). Лицензия MIT находится в `LICENSE`. Fixture специально +зафиксирован: тест Commander должен проверять запуск полноэкранного viewer, +PageDown/raw-режим и восстановление окружения после возврата, независимо от +последующих изменений проекта Examples. Обновлять снимок только вместе с +повторной проверкой сценария `mame_p5_viewer.lua` и эталонных кадров. diff --git a/applications/Volkov/tests/fixtures/mdview2/README.md b/applications/Volkov/tests/fixtures/mdview2/README.md new file mode 100644 index 0000000..0cc3c6a --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/README.md @@ -0,0 +1,153 @@ +# MDView v1.0 (b3) — просмотрщик Markdown для Sprinter + +**MDView** — программа просмотра документов Markdown для компьютера +**Sprinter-2000** (Z80, ОС ESTEX). Файл загружается в расширенную память (EMM) +и один раз «прогоняется» через парсер: готовые к показу строки (пары +символ+атрибут) складываются в **рендер-кэш** в EMM, после чего прокрутка в +любую сторону — это просто копирование готовых строк на экран, без повторного +парсинга. Даже на файлах в сотни килобайт листание остаётся мгновенным. + +Текстовый режим 80×32, цветное оформление элементов разметки, три режима +просмотра (MD / RAW / HEX) и четыре кодировки с автоопределением. + +--- + +## Возможности + +* **Markdown-рендеринг** с цветовым оформлением: + * заголовки `#`…`######` (H5/H6 отображаются как H4); + * **жирный** (`**текст**`), *курсив* (`*текст*`), подчёркнутый (`_текст_`), + ~~зачёркнутый~~ (`~~текст~~`), `встроенный код` (`` `текст` ``); + * экранирование `\*`, `\_`, `` \` `` и любой ASCII-пунктуации; + * чекбоксы `[x]` / `[ ]` в списках; + * ненумерованные (`-`, `*`, `+`) и нумерованные (`1.`, `1)`) списки + с базовой вложенностью по отступам; + * цитаты `>` (склейка многострочных, маркер │ на переносах); + * fenced-блоки кода ` ``` ` (без переносов, горизонтальный скролл); + * таблицы `| … | … |` — рисуются псевдографической рамкой, ширины колонок + вычисляются по содержимому (до 16 колонок); + * горизонтальные разделители `---` / `***` / `___`; + * жёсткие переносы (два пробела или `\` в конце строки); + * мягкая склейка абзацев с переносом по словам под ширину экрана. +* **Кодировки: CP866, CP1251, KOI8-R, UTF-8.** + * автоопределение при открытии (BOM → UTF-8; валидность multibyte-структуры; + частотный анализ ходовых русских букв для 8-битных); + * переключение по кругу клавишей **F8** в любой момент; + * 8-битные кодировки отличаются только перекодировкой глифов на отрисовке — + переключение мгновенно; + * UTF-8 декодируется в CP866 в **отдельный набор** (файл + индекс + кэш); + второй набор готовится **в фоне**, пока вы читаете документ, — обычно + к первому нажатию F8 он уже построен и переключение мгновенно, + с сохранением позиции. Если фон не успел, F8 докручивает начатую + сборку (со спиннером), а не начинает её заново. +* **Три режима просмотра:** + * **MD** — форматированный Markdown (по умолчанию); + * **RAW** (**F2**) — исходный текст без разметки: перенос строк кратно 80 + (**F3** — режим панорамы с горизонтальным скроллом); + * **HEX** (**F4**) — дамп *оригинального* файла: + `0x012340 │ 16 байт hex │ 16 печатных символов`. Печатная колонка + интерпретируется текущей кодировкой; для UTF-8 глиф ставится на позиции + лид-байта, continuation-байты показываются точкой. +* **Единая позиция** при любых переключениях: MD ↔ RAW ↔ HEX и смена + кодировки сохраняют текущее место в документе (между наборами разного + размера — пропорционально, с точностью до строки). +* **Прогрессивная загрузка**: первый экран показывается сразу, индексация + продолжается в фоне; по готовой части документа уже можно листать, + **Esc**/**F10** прерывают загрузку. +* **Фоновая работа незаметна**: второй набор кодировки строится только в + паузах между клавишами (после нажатия выдерживается пауза), поэтому + скролл — в том числе с автоповтором — не теряет плавности. + +--- + +## Запуск + +``` +MDVIEW2.EXE <файл.md> +``` + +Без аргумента открывается `README.MD` из текущего каталога. + +## Клавиши + +| Клавиша | Действие | +|--------------|-------------------------------------------------------------| +| ↑ / ↓ | прокрутка на одну строку | +| PgUp / PgDn | прокрутка на экран (30 строк) | +| Home / End | в начало / в конец документа | +| ← / → | горизонтальный сдвиг: код/таблицы в MD, панорама в RAW | +| F1 | справка | +| F2 | RAW-режим ↔ MD | +| F3 | в RAW: перенос строк ↔ панорама | +| F4 | HEX-режим ↔ прежний вид | +| F8 | кодировка: CP866 → CP1251 → KOI8-R → UTF-8 → … | +| Esc / F10 | выход (во время загрузки — прервать её) | + +Статус-бар (верхняя строка): имя файла, кодировка, диапазон видимых строк +и процент прокрутки. Нижняя строка — меню доступных F-клавиш. + +--- + +## Ограничения + +| Параметр | Значение | +|---------------------------------|-------------------------------------------| +| Размер файла | до 256 КБ (больший — обрезается с предупреждением) | +| Логических строк (после переносов) | до 18 432 | +| Длина строки в рендер-кэше | 255 ячеек (RAW/HEX ограничения не имеют) | +| Колонок в таблице | до 16 | +| Шаг табуляции | 4 | + +При исчерпании любого лимита документ завершается строкой-сообщением +с указанием причины обрыва; всё, что вошло, доступно для просмотра. + +## Требования + +* Sprinter-2000 с ОС ESTEX; +* расширенная память (EMM): в худшем случае (файл 256 КБ + оба набора + кодировок) — до ~150 страниц по 16 КБ (~2,4 МБ). Для типичных файлов + в десятки килобайт достаточно нескольких десятков страниц. + +Код, данные, стек и куча программы занимают окна W1+W2 (32 КБ, режим +памяти `small`); окно W3 используется только для доступа к EMM-страницам. + +--- + +## Сборка + +Требуется тулчейн этого репозитория (обёртка `sprinter-cc` над SDCC 4.5). +Из каталога `examples/mdview2`: + +``` +make # собрать mdview2.exe +make floppy # собрать и упаковать дискету для MAME (mc.img) +make run # floppy + автоматический прогон в MAME (скриншоты) +``` + +На дискету кладутся: `MDVIEW2.EXE` и три документа, каждый в своей кодировке +(заодно покрывают все пути автодетекта): `README.MD` — этот файл, как есть +(UTF-8); `DEMO.MD` — демонстрация всех элементов разметки (CP1251); +`CHANGES.MD` — история версий (CP866). + +## Структура исходников + +| Файл | Назначение | +|------------------|--------------------------------------------------------------| +| `mdview2.c` | ядро: EMM-аллокации, рендер-кэш, наборы кодировок, загрузка файла, главный цикл | +| `mdview2_index.c`| парсер/индексатор Markdown — единственный проход по файлу | +| `mdview2_md.c` | MD-вид: отрисовка из кэша, прокрутка | +| `mdview2_raw.c` | RAW-вид (F2/F3) | +| `mdview2_hex.c` | HEX-вид (F4) | +| `mdview2_enc.c` | кодировки: детект, ремап-таблицы, конвертер UTF-8 → CP866 | +| `mdview2_table.c`| отрисовка таблиц | +| `mdview2_status.c`| статус-бар, меню, спиннер | +| `mdview2_help.c` | справка (F1) | +| `mdview2_conf.h` | конфигурация: `WITH_RAW` / `WITH_HEX` (модули отключаемы) | +| `mdview2.h` | общие константы, атрибуты, межмодульный API | + +Подробности архитектуры — в `docs/mdview2-plan.md`. + +## Лицензия и авторы + +© 2026 Петров А.Г. Часть проекта Sprinter C Compiler +(см. LICENSE в корне репозитория). diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2.c b/applications/Volkov/tests/fixtures/mdview2/mdview2.c new file mode 100644 index 0000000..c4f2898 --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2.c @@ -0,0 +1,1338 @@ +/* + * mdview2 — просмотрщик Markdown для Sprinter (render-cache версия). + * + * Этот файл — ЯДРО (инфраструктура + точка входа). Прикладные подсистемы + * вынесены в отдельные модули (общий интерфейс — mdview2.h): + * mdview2_index.c — парсер/индексатор markdown (построение рендер-кэша) + * mdview2_md.c — представление MD-документа (отрисовка из кэша + скролл) + * mdview2_raw.c — RAW-просмотр исходного текста (F2/F3) + * mdview2_enc.c — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер + * mdview2_table.c — выровненная отрисовка таблиц + * mdview2_help.c — диалог справки (F1) + * + * В ядре остаётся: пул рендер-кэша, доступ к EMM/окну W3 (fb/win_rest), + * примитивы экрана, загрузка файла, наборы документа (doc-slots), кооператив- + * ная loading-loop и диспетчер клавиш, сборка наборов и main(). + * + * Разметка экрана (80x32): + * строка 0 — статус-бар (имя файла / диапазон строк / всего / %) + * строки 1..30 — область документа (30 видимых строк текста) + * строка 31 — меню (F1 Help / F2 RAW / F3 Wrap / F8 CodePg / F10 Exit) + * + * Память: + * CODE → W1 (режим sprinter-cc --memory small) + * STACK/HEAP/DATA → W2 + * Буфер файла → страницы EMM (до 8 × 16 КБ = 128 КБ), + * отображаемые в W3 по требованию через fb(). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "mdview2.h" /* геометрия, атрибуты, общий интерфейс модулей */ + +/* ---- Палитра атрибутов ------------------------------------------- */ + +static const uint8_t md_pallete[16 * 4] = { + 0x00, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, + 0x00, 0x60, 0x00, 0x00, + 0x60, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x60, 0x00, + 0x60, 0x00, 0x60, 0x00, + 0x00, 0x60, 0x60, 0x00, + 0xC8, 0xC8, 0xC8, 0x00, + 0xA0, 0xA0, 0x80, 0x00, + 0xFF, 0xAA, 0xAA, 0x00, + 0xAA, 0xFF, 0xAA, 0x00, + 0xFF, 0xFF, 0xAA, 0x00, + 0xAA, 0xAA, 0xFF, 0x00, + 0xFF, 0xAA, 0xFF, 0x00, + 0xAA, 0xFF, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF +}; + +/* Атрибуты экрана/справки и геометрия — в mdview2.h (общие со всеми модулями). + * Типы записей (idx_rec_t — в mdview2_index.c, cache_rec_t — в mdview2.h), + * словари флагов/стилей (IF_, CK_, INIT_STYLE_) и константы индекса + * (INDEX_RECS_PER_PAGE, MAX_CACHE_LINE_LEN) — в mdview2.h. + * + * В SDCC/z80 критично явно инициализировать static-переменные (`= 0`): + * неинициализированные static-объекты могут конфликтовать по размещению + * и перезаписывать друг друга (см. memory/sdcc_static_storage_gotcha). */ + +#define MAX_INDEX_PAGES 9u /* максимум 9 * 2048 = 18432 сегментов индекса (256 КБ) */ + +/* ================================================================== + * Рендер-кэш: директория и пул EMM-страниц + * ================================================================== + * Каждая логическая строка экрана рендерится РОВНО ОДИН РАЗ (Фаза 2) + * в готовый буфер и больше никогда не парсится повторно при скролле. + * Содержимое живёт в ОТДЕЛЬНОМ пуле EMM-страниц (cache_content_*), + * не путать с idx_rec_t/index_phys[] выше — те остаются неизменными, + * ими пользуется рендер-воркер ПРИ построении кэша (Фаза 2), и их + * нельзя перезатирать, т.к. воркер для одной строки может заглядывать + * в idx-записи СОСЕДНИХ строк (откат cont-сегментов). + * + * ЕДИНЫЙ формат для ВСЕХ строк (решено 2026-06-23 — изначальный план + * различал тип 1/тип 2 по экономии памяти, но строки таблиц — IF_NOWRAP, + * но НЕ IF_CODE — всё равно проходят inline-парсер и могут содержать + * несколько атрибутов, так что "один стиль" для них неверно; бюджет EMM + * (см. sprinter_emm_budget) с большим запасом покрывает (char,attr) + * для всех строк без исключения): контент — `len` пар (char,attr), + * `len` — реальная длина в ЯЧЕЙКАХ без паддинга до SCREEN_W, капается на + * 255 (патологически длинные nowrap-строки урезаются; индикатор обрезки + * добавляется на этапе вывода из кэша, не при рендере в кэш). + */ +/* cache_rec_t (РОВНО 8 байт, см. sprinter_winrest_format) — в mdview2.h. */ + +#define CACHE_DIR_REC_SIZE ((uint16_t)sizeof(cache_rec_t)) +#define CACHE_DIR_RECS_PER_PAGE (16384 / CACHE_DIR_REC_SIZE) /* 2048 */ +#define MAX_CACHE_DIR_PAGES 9u /* симметрично индексу: 9 * 2048 = 18432 записей */ +/* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */ +#define MAX_CACHE_CONTENT_PAGES 40u /* кап контента на набор: 40 * 16 КБ = 640 КБ (80 на оба набора) */ + +static uint8_t cache_dir_blk; +static uint8_t cache_dir_pages; +static uint8_t cache_dir_phys[MAX_CACHE_DIR_PAGES]; + +static uint8_t cache_content_blk[MAX_CACHE_CONTENT_PAGES]; /* id блока на каждую выделенную страницу */ +static uint8_t cache_content_phys[MAX_CACHE_CONTENT_PAGES]; +static uint8_t cache_content_n_pages; /* сколько страниц уже выделено */ +static uint8_t cache_cur_page_idx; /* индекс текущей страницы записи в cache_content_phys */ +static uint16_t cache_cur_off; /* текущее смещение записи в этой странице */ + +/* Доступ к директории рендер-кэша — тот же приём, что у idx_get/idx_put. */ +void cache_dir_get(uint16_t idx, cache_rec_t *r) +{ + uint8_t page = (uint8_t)(idx >> 11); + uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */ + bank_read(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); +} + +void cache_dir_put(uint16_t idx, const cache_rec_t *r) +{ + uint8_t page = (uint8_t)(idx >> 11); + uint16_t off = (uint16_t)((idx & 0x07FF) << 3); /* 2047 */ + bank_write(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); +} + +/* Резервирует nbytes подряд в пуле контента рендер-кэша. + * Запись никогда не разбивается через границу страницы: если остаток + * текущей страницы недостаточен — переходим на следующую целиком, + * жертвуя остатком (проще и безопаснее, чем page-crossing в fb()). + * При нехватке текущего пула — лениво доращиваем его на 1 страницу. + * Возврат: 1 — успех (out_page и out_off заполнены), 0 — EMM кончилась. */ +uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off) +{ + if (nbytes == 0 || nbytes > PAGE_SIZE) return 0; + + if (cache_content_n_pages == 0 || + (uint16_t)(cache_cur_off + nbytes) > PAGE_SIZE) { + /* Нужна новая страница: либо пул ещё пуст, либо текущая страница + * не вмещает запись целиком. */ + uint8_t next_idx = (cache_content_n_pages == 0) ? 0 : (uint8_t)(cache_cur_page_idx + 1); + if (next_idx >= cache_content_n_pages) { + if (cache_content_n_pages >= MAX_CACHE_CONTENT_PAGES) + return 0; + uint8_t blk = mem_alloc_pages(1); + if (blk == 0) + return 0; + cache_content_blk[cache_content_n_pages] = blk; + cache_content_phys[cache_content_n_pages] = mem_get_page(blk, 0); + cache_content_n_pages++; + } + cache_cur_page_idx = next_idx; + cache_cur_off = 0; + } + + *out_page = cache_content_phys[cache_cur_page_idx]; + *out_off = cache_cur_off; + cache_cur_off = (uint16_t)(cache_cur_off + nbytes); + return 1; +} + +/* Копирует уже готовый буфер (рендер одной строки, собранный в W1/W2 — + * НЕ в W3, чтобы не конфликтовать с fb()/чтением исходника во время + * рендера) в зарезервированное место в пуле контента. Один bank_write + * на строку, а не по байту. len — РОВНО как у cache_reserve()/bank_write + * — uint16_t, не uint8_t: строка до MAX_CACHE_LINE_LEN=255 ячеек даёт + * до 510 байт (char,attr), это уже не лезет в uint8_t. */ +void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len) +{ + bank_write(page, off, buf, len); +} + +/* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */ +/* emph_to_attr — в mdview2_index.c. */ + +uint16_t n_lines; /* extern: всего строк в индексе текущего набора */ +uint16_t max_lines; /* extern: ёмкость индекса: index_pages * 2048 */ +uint16_t top_line; /* extern: верхняя видимая строка */ +uint32_t file_size; /* extern: размер активного буфера документа */ +static uint8_t file_blk; +static uint8_t file_pages; +static uint8_t file_phys[MAX_PAGES]; +uint8_t index_blk; /* extern: дескриптор EMM-блока индекса */ +uint8_t index_pages; /* */ +uint8_t index_phys[MAX_INDEX_PAGES]; /* extern: физ. страницы индекса */ +uint8_t index_truncated; /* выставляется при исчерпании ёмкости индекса */ +uint8_t g_file_clamped; /* extern: файл >256 КБ обрезан до MAX_FILE при загрузке */ + +/* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */ +uint8_t cur_page = 0xFF; /* extern: 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */ +uint8_t viewport_x; /* extern: горизонтальный сдвиг для nowrap-строк */ +char filename[64]; /* extern: статус-бар */ + +/* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока + * index_lines() ещё досканирует файл (кооперативно, без прерываний). */ +uint8_t g_view = VIEW_MD; /* extern: активный режим просмотра (существует даже когда доступен только MD режим) */ +#if WITH_RAW +uint8_t g_ready; /* extern: 1 когда первичный документ построен — F2 (RAW) рабочий */ +static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */ +#if WITH_HEX +static uint8_t g_hex_last = VIEW_MD; /* вид, из которого вошли в HEX (возврат по F4) */ +#endif +#endif +uint8_t g_loading; /* extern: 1 во время видимой индексации (build_doc) */ +static uint8_t g_first_drawn; /* первый экран уже нарисован */ +uint8_t g_f8_enabled; /* extern: можно ли сейчас переключать кодировку */ +static uint8_t g_utf_failed; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */ +uint8_t g_bg_building; /* extern: 1 во время фонового index_step — гасит спиннер индексатора */ +static uint8_t g_bg_doc = 0xFF; /* слот, строящийся в фоне (bg_step); 0xFF = фоновой сборки нет */ +uint8_t g_abort; /* extern: F10/Esc во время загрузки → прервать индексацию */ +static uint16_t g_last_prog; /* n_lines на последнем обновлении статуса */ + +/* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */ +const uint8_t *g_remap; /* extern: активная таблица ремапа (CP1251/KOI8), 0 = без ремапа (CP866)*/ +uint8_t g_scratch_phys; /* extern: EMM-страница под ремап-буфер строки для win_rest */ +static uint8_t g_scratch_blk; /* handle scratch-блока: нужен для освобождения при exit */ + +/* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его + * состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h. + * build_doc() ниже перенаправляет живой file_* на utf_phys и оркеструет + * инкрементальную конвертацию (utf_conv_reset/utf_convert_more). */ + +/* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его + * как есть, набор DOC_UTF8 читает utf_phys. */ +static uint8_t orig_file_blk; +uint8_t orig_file_pages; /* extern: читает кодек (utf_alloc) */ +uint32_t orig_file_size; /* extern: читает кодек (utf_convert_more) */ +uint8_t orig_file_phys[MAX_PAGES]; /* extern: читает кодек (cv_read) */ + +/* ----------------- Два сосуществующих набора индекс/кэш ----------------- + * DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на + * отрисовке); DOC_UTF8 — UTF-8, декодированный в CP866. Оба строятся eager + * при старте (кооперативно), поэтому F8 переключает активный набор мгновенно. + * fb()/idx/cache-аксессоры работают по «живым» глобалам; doc_switch() свапает + * их с сохранённой копией нужного набора. */ +typedef struct { + uint8_t built; + uint8_t enc; /* кодировка отображения этого набора */ + uint8_t file_blk, file_pages; + uint32_t file_size; + uint8_t file_phys[MAX_PAGES]; + uint8_t index_blk, index_pages, index_truncated; + uint16_t n_lines, max_lines; + uint8_t index_phys[MAX_INDEX_PAGES]; + uint8_t cache_dir_blk, cache_dir_pages; + uint8_t cache_dir_phys[MAX_CACHE_DIR_PAGES]; + uint8_t cache_content_n_pages, cache_cur_page_idx; + uint16_t cache_cur_off; + uint8_t cache_content_blk[MAX_CACHE_CONTENT_PAGES]; + uint8_t cache_content_phys[MAX_CACHE_CONTENT_PAGES]; + uint16_t top_line; + uint8_t viewport_x; +} docset_t; + +#define DOC_8BIT 0 +#define DOC_UTF8 1 +static docset_t g_doc[2]; /* built=0 для обоих: _DATA зануляется в crt0 gsinit + * (с 2026-07-04), явный {0}-инициализатор больше + * не нужен — он стоил ~260 байт _INITIALIZER */ +static uint8_t g_active_doc = DOC_8BIT; /* какой набор сейчас в живых глобалах */ +static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показан пользователю */ + +static void doc_switch(uint8_t target); /* forward (нужна в progress_tick) */ + +/* Статус-бар, меню и спиннер — в mdview2_status.c. */ + +/* ================================================================== + * Примитивы экрана, доступ к EMM/окну W3 (fb/win_rest), палитра + * ================================================================== */ + +/* Печатает строку с указанным атрибутом, начиная с позиции (x,y), + * и останавливается на конце строки или правой границе экрана. + * `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его + * можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */ +void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) +{ + uint8_t len = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0; + len = min(len, strlen(s)); + if (len == 0) return; + bios_set_place(y, x); + bios_writeattr_until(s, len, attr, 0); +} + + +/* Полностью очищает указанную строку экрана символами пробела с attr. */ +void fill_row(uint8_t y, uint8_t attr) +{ + bios_clearwin(y, 0, 1, SCREEN_W, attr); +} + +/* Процент 0..100 = num*100/den на чистой 16-битной арифметике — чтобы НЕ + * тянуть 32-битное деление __divulong (~760 Б) ради показа процента в статусе. + * Оба вызова (MD/RAW) дают операнды, влезающие в 16 бит (≤18432 и ≤1024), + * переполняет только *100 — поэтому масштабируем оба вниз, пока num*100 не + * влезёт в uint16, затем считаем долю циклом-вычитанием (без div-хелпера). + * Инвариант вызовов: num ≤ den; результат всё равно ограничен 100. */ +uint8_t pct16(uint16_t num, uint16_t den) +{ + if (den == 0) return 0; + while (num > 655) { num >>= 1; den >>= 1; } /* num*100 ≤ 65500 < 65536 */ + if (den == 0) return 100; /* страховка от num>den */ + uint16_t n = (uint16_t)(num * 100u); + uint8_t p = 0; + while (n >= den && p < 100) { n = (uint16_t)(n - den); p++; } + return p; +} + + +/* Отображает указанную EMM-страницу файла в окно W3. + * Если эта страница уже активна, переключение не выполняется. */ +// static inline void map_page(uint8_t page) +// { +// if (page != cur_page) { +// /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */ +// sprinter_page_w3(file_phys[page]); +// cur_page = page; +// } +// } + + +/* Читает один байт логического файла по смещению p. + * При переходе между страницами автоматически переключает W3. + * Проверка границ выполняется вызывающей стороной. + * + * Эквивалент C-кода (оставлен для справки): + * uint8_t *pb = (uint8_t *)&p; + * uint8_t page = (pb[1] >> 6) | (pb[2] << 2); + * uint16_t off = ((pb[1] & 0x3F) << 8) | pb[0]; + * map_page(page); // if (page!=cur_page){cur_page=page; OUT W3,file_phys[page];} + * return FILE_BUF[off]; // *(0xC000 + off) + * + * Ручной asm — это горячая примитивная операция (вызывается сотни тысяч раз + * при индексации 50КБ-файла); C-версия тратила вызов функции + 32-битную + * арифметику на КАЖДЫЙ байт. Раскладка __sdcccall(1): аргумент uint32_t в + * HLDE (E=pb[0], D=pb[1], L=pb[2], H=pb[3] — проверено sdcc -S), возврат + * char в A. Координация маппинга с bank_read/bank_write держится на том, + * что _io_page_w3 — это ПОРТ (__sfr 0xE2): здесь пишем его через OUT, + * bank_read читает через IN — общий аппаратный регистр, без теневой RAM. + * NB: __naked обязателен (внутри собственный ret); IX/IY не трогаем + * (callee-saved), BC/DE/HL/AF — caller-saved, можно клобберить. */ +char fb(uint32_t p) __naked +{ + (void)p; /* p = HLDE; доступ только из asm */ + __asm + ; off -> BC = 0xC000 + ((pb[1]&0x3F)<<8 | pb[0]) ; D=pb[1], E=pb[0] + ld a, d + or a, #0xC0 ; (D|0xC0) == (D&0x3F)|0xC0 — старший байт окна W3 + ld b, a + ld c, e ; C = pb[0] + + ; page = (pb[1]>>6) | (pb[2]<<2) ; L=pb[2] + ld a, l + add a, a + add a, a ; A = pb[2]<<2 + ld e, a ; E свободен (pb[0] уже сохранён в C) + + ld a, d + rlca + rlca + and a, #0x03 ; A = pb[1]>>6 + or a, e ; A = page + ld e, a ; E = page + + ; map_page(page) + ld a, (_cur_page) + cp e + jr Z, fb_same + ld a, e + ld (_cur_page), a + ld hl, #_file_phys + ld d, #0x00 + add hl, de ; hl = &file_phys[page] + ld a, (hl) + out (#0xE2), a ; OUT PORT_PAGE_W3 (== _io_page_w3 sfr) + fb_same: + ld a, (bc) ; A = *(0xC000 + off) + ret + __endasm; +} + +/* ESTEX WINREST (5Ah, RST 0x10) — копирует готовый буфер (char,attr) + * из смещения `off` ВНУТРИ EMM-страницы `page` прямо в текстовое окно + * экрана, минуя любые BIOS print-вызовы. Формат буфера подтверждён + * эмпирически в MAME (см. memory/sprinter_winrest_format.md и + * tests/winrest/winrest.c): пары (char,attr) по ячейкам, по строкам, + * шаг строки = width*2, без паддинга. row/col — левый верхний угол + * вывода на экране, h/w — высота/ширина в символах. + * + * Регистровая раскладка SDCC __sdcccall(1) для (row,col,h,w,page,off) + * проверена через `sdcc -S` (пробный non-naked враппер): A=row, L=col, + * стек [SP+2]=h [SP+3]=w [SP+4]=page [SP+5..6]=off (little-endian). + * + * История: до 2026-06-23 `off` не было — IX всегда указывал на самое + * начало страницы (#0xC000), это работало только пока в кэше было + * РОВНО ОДНА запись на странице со смещением 0 (Фаза 1 self-test). + * Когда Фаза 2 начала пакетировать много строк на одной странице с + * РАЗНЫМИ off, всё стало читать с начала страницы (т.е. контент первой + * записи) независимо от реальной строки — добавлен off, IX = 0xC000+off. */ +void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint16_t off) __naked +{ + (void)row; (void)col; (void)h; (void)w; (void)page; (void)off; + __asm + ld iy, #2 + add iy, sp + ld d, a ; D = row + ld e, l ; E = col + push ix ; сохранить IX вызывающей функции (callee-saved) + ld l, 3 (iy) ; off (младший байт), SP+5 + ld h, 4 (iy) ; off (старший байт), SP+6 + ld bc, #0xC000 + add hl, bc + push hl + pop ix ; IX = 0xC000 + off + ld h, 0 (iy) ; H = height + ld a, 1 (iy) ; width -> L + ld l, a + ld a, 2 (iy) ; page -> B + ld b, a + ld c, #0x5A ; ESTEX WINREST + di + rst #0x10 + ei + pop ix ; восстановить IX вызывающей функции + pop hl ; адрес возврата + inc sp + inc sp + inc sp + inc sp + inc sp ; снять h,w,page,off (1+1+1+2 байт) со стека + jp (hl) + __endasm; +} + +/* Char-хелперы, idx_get/idx_put/seg_off, set_*_cur — в mdview2_index.c. */ + +/* Загружает 16 текстовых атрибутов в палитры ink/paper/blink. */ +static void set_pallete(void) { + uint8_t buff[64]; + for(int i = 0; i < 16; i++) { + for(int j = 0; j < 16; j++) { + buff[j * 4] = md_pallete[i * 4]; + buff[j * 4 + 1] = md_pallete[i * 4 + 1]; + buff[j * 4 + 2] = md_pallete[i * 4 + 2]; + buff[j * 4 + 3] = md_pallete[i * 4 + 3]; + } + + text_pal_load(TEXT_PAL_INK, i * 16, 16, md_pallete); + text_pal_load(TEXT_PAL_BLINK_INK, i * 16, 16, md_pallete); + text_pal_load(TEXT_PAL_PAPER, i * 16, 16, buff); + text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16, 8, buff + 32); + text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16 + 8, 8, buff + 32); + } +} + +/* ================================================================== + * Загрузка/выгрузка файла + * ================================================================== */ + +/* Загружает исходные байты файла в EMM-страницы (без индекса/кэша — те + * выделяются отдельно на КАЖДЫЙ набор через alloc_set_storage()). + * Возврат: 0 — успех, отрицательное значение — код ошибки. */ +static int load_file(const char *path) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + + long sz = lseek(fd, 0, SEEK_END); + if (sz < 0) { + close(fd); + return -2; + } + /* Файл больше MAX_FILE (256 КБ) не отвергаем, а КЛАМПим: читаем первые + * 256 КБ, индексатор допишет строку-предупреждение (TRUNC_FILE). */ + g_file_clamped = 0; + if ((uint32_t)sz > MAX_FILE) { + sz = (long)MAX_FILE; + g_file_clamped = 1; + } + + file_size = (uint32_t)sz; + lseek(fd, 0, SEEK_SET); + + /* Выделяем ровно столько EMM-страниц, сколько нужно под файл. */ + file_pages = (uint8_t)((file_size + PAGE_SIZE - 1u) / PAGE_SIZE); + if (file_pages == 0) file_pages = 1; + file_blk = mem_alloc_pages(file_pages); + if (file_blk == 0) { + close(fd); + return -3; + } + + /* Читаем файл кусками по 16 КБ, последовательно маппя страницы в W3. + * На последней итерации читаем остаток меньше PAGE_SIZE. */ + uint32_t remaining = file_size; + for (uint8_t page = 0; page < file_pages; page++) { + spinner_tick(); + uint8_t phys = mem_get_page(file_blk, page); + file_phys[page] = phys; + sprinter_page_w3(phys); + cur_page = page; + uint16_t chunk = (remaining > PAGE_SIZE) + ? (uint16_t)PAGE_SIZE + : (uint16_t)remaining; + int n = read(fd, FILE_BUF, chunk); + if (n != (int)chunk) { + close(fd); + return -4; + } + remaining -= chunk; + } + close(fd); + return 0; +} + +/* Выделяет в живые глобалы блок индекса + директорию рендер-кэша под текущий + * набор. file_pages должно быть уже выставлено (размер набора). Сбрасывает + * пул контента в пустое состояние (страницы доращиваются лениво при рендере). + * Возврат: 0 — успех, -5/-6 — нехватка EMM. */ +static int alloc_set_storage(void) +{ + uint8_t want; + + want = (uint8_t)((file_pages >> 1) + 1u); + if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; + index_blk = mem_alloc_pages(want); + if (index_blk == 0) return -5; + index_pages = want; + for (uint8_t i = 0; i < index_pages; i++) + index_phys[i] = mem_get_page(index_blk, i); + max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE; + + want = (uint8_t)((file_pages >> 1) + 1u); + if (want > MAX_CACHE_DIR_PAGES) want = MAX_CACHE_DIR_PAGES; + cache_dir_blk = mem_alloc_pages(want); + if (cache_dir_blk == 0) return -6; + cache_dir_pages = want; + for (uint8_t i = 0; i < cache_dir_pages; i++) + cache_dir_phys[i] = mem_get_page(cache_dir_blk, i); + + cache_content_n_pages = 0; + cache_cur_page_idx = 0; + cache_cur_off = 0; + return 0; +} + +static void doc_save_live(uint8_t i); /* forward */ + +/* Освобождает все EMM-блоки обоих наборов (индекс/директория/пул контента) + * плюс два файловых буфера (исходный 8-битный и UTF-8). */ +static void unload_file(void) +{ + doc_save_live(g_active_doc); /* зафиксировать живой набор в g_doc */ + for (uint8_t s = 0; s < 2; s++) { + docset_t *d = &g_doc[s]; + if (d->index_blk) { mem_free_block(d->index_blk); d->index_blk = 0; } + if (d->cache_dir_blk) { mem_free_block(d->cache_dir_blk); d->cache_dir_blk = 0; } + for (uint8_t i = 0; i < d->cache_content_n_pages; i++) + if (d->cache_content_blk[i]) mem_free_block(d->cache_content_blk[i]); + d->cache_content_n_pages = 0; + } + if (orig_file_blk) { mem_free_block(orig_file_blk); orig_file_blk = 0; } + if (utf_blk) { mem_free_block(utf_blk); utf_blk = 0; } + if (g_scratch_blk) { mem_free_block(g_scratch_blk); g_scratch_blk = 0; } + file_blk = 0; +} + +/* Парсер/индексатор (classify_line, scan_join_stream, inline_scan, index_lines, emph_to_attr) — в mdview2_index.c. */ + + +/* Таблицы ремапа, set_encoding, кодировки и UTF-8 конвертер — в mdview2_enc.c. */ + +/* ================================================================== + * Наборы документа (doc-slots): сохранение/свап + * ================================================================== + * Два сосуществующих набора (DOC_8BIT / DOC_UTF8) живут как копии всего + * состояния (file/index/cache/позиция) в g_doc[]; «живые» глобалы — это + * активный набор. doc_save_live: живые → g_doc[i]; doc_load_live: g_doc[i] + * → живые (+ инвалидация маппинга fb() и активной кодировки). */ +static void doc_save_live(uint8_t i) +{ + docset_t *d = &g_doc[i]; + d->enc = g_encoding; + d->file_blk = file_blk; d->file_pages = file_pages; d->file_size = file_size; + memcpy(d->file_phys, file_phys, MAX_PAGES); + d->index_blk = index_blk; d->index_pages = index_pages; d->index_truncated = index_truncated; + d->n_lines = n_lines; d->max_lines = max_lines; + memcpy(d->index_phys, index_phys, MAX_INDEX_PAGES); + d->cache_dir_blk = cache_dir_blk; d->cache_dir_pages = cache_dir_pages; + memcpy(d->cache_dir_phys, cache_dir_phys, MAX_CACHE_DIR_PAGES); + d->cache_content_n_pages = cache_content_n_pages; + d->cache_cur_page_idx = cache_cur_page_idx; d->cache_cur_off = cache_cur_off; + memcpy(d->cache_content_blk, cache_content_blk, MAX_CACHE_CONTENT_PAGES); + memcpy(d->cache_content_phys, cache_content_phys, MAX_CACHE_CONTENT_PAGES); + d->top_line = top_line; d->viewport_x = viewport_x; +} + +static void doc_load_live(uint8_t i) +{ + docset_t *d = &g_doc[i]; + file_blk = d->file_blk; file_pages = d->file_pages; file_size = d->file_size; + memcpy(file_phys, d->file_phys, MAX_PAGES); + index_blk = d->index_blk; index_pages = d->index_pages; index_truncated = d->index_truncated; + n_lines = d->n_lines; max_lines = d->max_lines; + memcpy(index_phys, d->index_phys, MAX_INDEX_PAGES); + cache_dir_blk = d->cache_dir_blk; cache_dir_pages = d->cache_dir_pages; + memcpy(cache_dir_phys, d->cache_dir_phys, MAX_CACHE_DIR_PAGES); + cache_content_n_pages = d->cache_content_n_pages; + cache_cur_page_idx = d->cache_cur_page_idx; cache_cur_off = d->cache_cur_off; + memcpy(cache_content_blk, d->cache_content_blk, MAX_CACHE_CONTENT_PAGES); + memcpy(cache_content_phys, d->cache_content_phys, MAX_CACHE_CONTENT_PAGES); + top_line = d->top_line; viewport_x = d->viewport_x; + cur_page = 0xFF; /* следующий fb() пере-смаппит страницу */ + set_encoding(d->enc); +} + +static void doc_switch(uint8_t target) +{ + if (target == g_active_doc) + return; + doc_save_live(g_active_doc); + doc_load_live(target); + g_active_doc = target; +} + +/* Автоопределение/имя кодировки, ремап-вывод и UTF-8→CP866 конвертер — в mdview2_enc.c. */ + +/* Отрисовка области/статус-бара/меню и прокрутка — в модуле mdview2_md.c. */ + + + +/* ================================================================== + * Прогрессивная загрузка и диспетчер клавиш (MD) + * ================================================================== + * index_lines() строит индекс кооперативно и периодически зовёт + * progress_tick(): тот рисует первый готовый экран, крутит спиннер и + * обрабатывает клавиши на лету (load_key). Диалог справки (F1) — в + * mdview2_help.c, RAW-навигация — в mdview2_raw.c. */ + +/* Обработка клавиши во время прогрессивной загрузки: скролл строго в + * пределах готовых строк (drawable_lines); End = последняя готовая страница; + * F1 = справка (show_help сам блокирует и перерисовывает). */ +static void load_key(uint16_t k) +{ + uint8_t ascii = (uint8_t)(k & 0xFF); + uint8_t scan = (uint8_t)((k >> 8) & 0x7F); + uint16_t dr = drawable_lines(); + uint16_t maxtop = (dr > VIEW_H) ? (uint16_t)(dr - VIEW_H) : 0; + uint16_t nt = top_line; + + if (ascii == 0x1B || scan == KEY_F10) { + g_abort = 1; + return; + } /* Esc/F10 → выход */ + if (ascii) + return; /* прочие ascii игнорируем */ + + switch (scan) { + case KEY_UP: + nt = (top_line >= 1) ? (uint16_t)(top_line - 1) : 0; + break; + case KEY_DOWN: + nt = top_line + 1; + break; + case KEY_PGUP: + nt = (top_line >= VIEW_H) ? (uint16_t)(top_line - VIEW_H) : 0; + break; + case KEY_PGDN: + nt = top_line + VIEW_H; + break; + case KEY_HOME: + top_line = 0; + viewport_x = 0; + draw_viewport_from_cache(); + render_md_status_numbers(); + return; + case KEY_END: + top_line = maxtop; + viewport_x = 0; + draw_viewport_from_cache(); + render_md_status_numbers(); + return; + case KEY_LEFT: + md_scroll_horizon(-(int8_t)HPAN_STEP); + render_md_status_numbers(); + return; + case KEY_RIGHT: + md_scroll_horizon(+(int8_t)HPAN_STEP); + render_md_status_numbers(); + return; + case KEY_F1: + show_help(); + return; + case KEY_F8: + /* во время сборки крутим ТОЛЬКО 8-бит и только если набор 8-битный + * (ремап на отрисовке; индексация не зависит от кодировки) */ + if (g_f8_enabled && g_active_doc == DOC_8BIT) { + uint8_t nx = (g_encoding == ENC_CP866) ? ENC_CP1251 : + (g_encoding == ENC_CP1251) ? ENC_KOI8R : ENC_CP866; + set_encoding(nx); + g_doc[DOC_8BIT].enc = nx; + draw_viewport_from_cache(); + render_full_status(); + } + return; + default: + return; + } + + if (nt > maxtop) + nt = maxtop; + if (nt != top_line) { + top_line = nt; + draw_viewport_from_cache(); + render_md_status_numbers(); + } +} + +/* Кооперативный «тик» прогрессивной загрузки — вызывается из index_lines(). */ +void progress_tick(void) +{ +#if WITH_RAW + /* В RAW-режиме буфер документа готов сразу после конвертации (до индекса), + * поэтому первый экран RAW можно показать немедленно — рисуем raw_draw, а + * не MD-вид (иначе мелькал бы MD). Индекс при этом строится дальше (нужен + * для MD-вида). */ + if (g_view != VIEW_MD) { +#if WITH_HEX + if (g_view == VIEW_HEX) { /* HEX показывает оригинал — экран уже актуален */ + spinner_tick(); + return; + } +#endif + if (!g_first_drawn) { + raw_draw(); + // render_raw_status(); + g_first_drawn = 1; + cur_page = 0xFF; /* raw_draw сбил W3-маппинг, на который опирается fb() */ + } + spinner_tick(); + return; + } +#endif + /* Прогрессивная отрисовка строящегося (всегда показываемого) набора. */ + uint8_t touched = 0; + if (!g_first_drawn && drawable_lines() >= VIEW_H) { + draw_viewport_from_cache(); + render_md_status_numbers(); + g_first_drawn = 1; + touched = 1; + } + if (n_lines >= (uint16_t)(g_last_prog + 10)) { + g_last_prog = n_lines; + spinner_tick(); + if (g_first_drawn) + render_md_status_numbers(); + touched = 1; + } + if (g_first_drawn && kbhit()) { + load_key(getkey()); + touched = 1; + } + /* Отрисовка/WINREST/BIOS могли сбить маппинг W3, на который опирается fb() + * при индексации → форсируем ре-маппинг страницы файла на следующем fb(). */ + if (touched) + cur_page = 0xFF; +} + +/* ================================================================== + * Точка входа + * ================================================================== */ + +/* Строит набор `slot` кооперативно (index_step + progress_tick). + * visible=1 — первичный (показываемый): прогрессивная отрисовка первого экрана + * и скролл по готовым строкам. visible=0 — вторичный: индексация идёт в живых + * глобалах, а progress_tick через свап показывает/листает уже готовый + * g_disp_doc. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). + * + * Если slot прямо сейчас строится ФОНОМ (g_bg_doc, см. bg_step ниже) — + * не начинаем заново, а ДОКРУЧИВАЕМ: загружаем частично построенный набор + * и продолжаем с сохранённой позиции индексатора (его межшаговое состояние + * живёт в статиках модуля index и переживает свапы наборов). + * + * UTF-8 набор конвертируется ИНКРЕМЕНТАЛЬНО: выделяем страницы, переключаем + * живой буфер на utf и запускаем конвертацию первого чанка; дальше index_step + * докручивает её впереди позиции чтения (g_utf_building). Конвертер читает + * оригинал из orig_file_phys[] через cv_read (не через fb, который теперь + * указывает на utf). */ +/* Настраивает живые глобалы под сборку набора slot: файловый буфер (utf/orig), + * индекс + директория кэша, сброс индексатора. Общая часть build_doc и + * bg_build_start. Возврат: 0 — успех, <0 — нехватка EMM (-5/-6/-7). */ +static int doc_setup(uint8_t slot) +{ + if (slot == DOC_UTF8) { + if (!utf_avail) { /* первый вход: выделить страницы */ + if (!utf_alloc()) + return -7; /* выделяет страницы + сбрасывает конвертер */ + utf_avail = 1; + } + file_blk = utf_blk; + file_pages = utf_pages; + memcpy(file_phys, utf_phys, MAX_PAGES); + if (g_utf_building) { + file_size = 0; + cur_page = 0xFF; + utf_convert_more(CONV_MARGIN); /* первый чанк — для первого экрана */ + } else { + file_size = utf_size; + } + } else { + file_blk = orig_file_blk; + file_pages = orig_file_pages; + file_size = orig_file_size; + memcpy(file_phys, orig_file_phys, MAX_PAGES); + } + cur_page = 0xFF; + + int rc = alloc_set_storage(); + if (rc < 0) + return rc; + index_begin(); + return 0; +} + +static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) +{ + if (g_bg_doc == slot) { /* фон уже строит его — продолжить */ + doc_load_live(slot); + g_bg_doc = 0xFF; + } else { + int rc = doc_setup(slot); + if (rc < 0) + return rc; + } + + set_encoding(enc); + top_line = 0; + viewport_x = 0; + g_active_doc = slot; + if (visible) { + g_disp_doc = slot; + g_first_drawn = 0; + status_encoding(); /* показать кодировку сразу (детект/F8), не дожидаясь индексации */ + cur_page = 0xFF; /* BIOS-вывод мог сбить W3-маппинг, на который опирается fb() */ + } + + g_loading = 1; + g_last_prog = 0; + while (!index_step(255)) {} + g_loading = 0; + + /* Зафиксировать конвертацию (на случай обрыва индекса по max_lines до конца + * исходника) — иначе g_utf_building остался бы 1 и сломал бы следующую сборку. */ + if (g_utf_building) { + utf_size = file_size; + g_utf_building = 0; + } + + g_doc[slot].built = 1; + doc_save_live(slot); + return 0; +} + +/* ================================================================== + * Фоновая сборка второго набора (idle-time, между клавишами) + * ================================================================== + * Пока пользователь читает документ, главный цикл в паузах между клавишами + * докручивает индексацию второго набора (UTF-8 при 8-битном первичном и + * наоборот) порциями index_step: свап на bg-набор → несколько итераций → + * свап обратно. Экран при этом не трогается (g_bg_building гасит спиннер), + * задержка реакции на клавишу — не больше одного шага. По готовности F8 + * переключает наборы мгновенно; F8 ДО готовности докручивает сборку видимо + * (ветка resume в build_doc), не начиная её заново. */ + +#define BG_STEP_BUDGET 2u /* итераций индексатора на idle-шаг: латентность клавиши + * = длительность шага (~единицы мс на обычном абзаце) */ +#define BG_KEY_HOLDOFF 300u /* пустых опросов kbhit после клавиши до возобновления + * фона — при автоповторе (скролл) фон не вклинивается */ + +/* Готовит фоновую сборку второго набора: настраивает живые глобалы (буфер, + * индекс, кэш, index_begin) так же, как build_doc, но БЕЗ индексации, и + * возвращает живым прежний набор. При нехватке EMM фон просто не стартует — + * остаётся сегодняшний ленивый путь по F8. */ +static void bg_build_start(void) +{ + uint8_t slot = (uint8_t)((g_active_doc == DOC_8BIT) ? DOC_UTF8 : DOC_8BIT); + uint8_t vis = g_active_doc; + if (g_doc[slot].built || (slot == DOC_UTF8 && g_utf_failed)) + return; /* строить нечего */ + + doc_save_live(vis); + if (doc_setup(slot) < 0) { /* нет EMM — как и в ленивом пути */ + if (slot == DOC_UTF8) g_utf_failed = 1; + doc_load_live(vis); + return; + } + top_line = 0; + viewport_x = 0; + + g_active_doc = slot; /* живые глобалы = bg-набор... */ + doc_switch(vis); /* ...сохранить его и вернуть видимый */ + g_bg_doc = slot; +} + +/* Один idle-шаг фона: свап на строящийся набор, порция индексации, свап + * обратно. Межшаговое состояние индексатора/UTF-конвертера — статики их + * модулей, обработчики клавиш видимого набора их не трогают. По завершении + * финализирует набор (дальше F8 переключается мгновенно). */ +static void bg_step(void) +{ + uint8_t vis = g_active_doc; + doc_switch(g_bg_doc); + g_bg_building = 1; + uint8_t done = index_step(BG_STEP_BUDGET); + g_bg_building = 0; + if (done) { + if (g_utf_building) { /* как build_doc: обрыв индекса до конца исходника */ + utf_size = file_size; + g_utf_building = 0; + } + g_doc[g_bg_doc].built = 1; /* doc_save_live поле built не трогает */ + g_bg_doc = 0xFF; + } + doc_switch(vis); +} + +/* Пропорциональный перенос байт-позиции между буферами разного размера + * (8bit ↔ UTF-8 при F8): off * to / from БЕЗ __mullong/__divulong. Один + * цикл restoring-деления: бит k дроби off/from (сравнение с from/2^k) + * сразу добавляет to/2^k в результат — сумма to/2^k по установленным + * битам и есть off*to/from. Точность from/65536 (≤4 байта на максимальном + * файле); ляжет на границу строки через line_at_off / raw_reanchor. */ +static uint32_t map_off(uint32_t off, uint32_t from, uint32_t to) +{ + if (from == to || from == 0) return off; + uint32_t res = 0; + for (uint8_t i = 0; i < 16; i++) { + from >>= 1; + to >>= 1; + if (off >= from) { off -= from; res += to; } + } + return res; +} + +/* Байт-позиция текущего вида (верх экрана) — единая «валюта» для переноса + * позиции между видами/наборами. Валюта = offset АКТИВНОГО буфера; HEX живёт + * в байтах оригинала, для UTF-набора конвертируем через map_off. */ +static uint32_t view_pos(void) +{ +#if WITH_HEX + if (g_view == VIEW_HEX) + return (g_active_doc == DOC_UTF8) + ? map_off(hex_pos(), orig_file_size, file_size) + : hex_pos(); +#endif +#if WITH_RAW + if (g_view != VIEW_MD) return raw_pos(); +#endif + return seg_off(top_line); +} + +/* Поставить текущий вид на байт-позицию off (активного буфера). */ +static void view_reanchor(uint32_t off) +{ +#if WITH_HEX + if (g_view == VIEW_HEX) { + hex_reanchor((g_active_doc == DOC_UTF8) + ? map_off(off, file_size, orig_file_size) + : off); + return; + } +#endif +#if WITH_RAW + if (g_view != VIEW_MD) { raw_reanchor(off); return; } +#endif + top_line = line_at_off(off); +} + +/* Переключает кодировку на `next` (вызывается из главного цикла по F8). + * 8-бит↔8-бит внутри одного набора — мгновенный ремап; переход в другой набор + * — мгновенный doc_switch, если он построен, иначе ЛЕНИВАЯ сборка по месту + * (со спиннером), результат кэшируется. */ +/* Меняет кодировку: тот же набор — ремап; другой готовый — doc_switch; + * непостроенный — ЛЕНИВАЯ ТИХАЯ сборка (спиннер, без отрисовки документа). + * НЕ перерисовывает документ/статус — это делает вызывающий в своём режиме + * (MD: draw_viewport_from_cache; RAW: raw_draw), чтобы не мелькал чужой вид. */ +static void switch_encoding(uint8_t next) +{ + uint8_t target = (next == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT; + + if (target == g_active_doc) { /* тот же набор — только ремап */ + set_encoding(next); + g_doc[target].enc = next; + } else if (g_doc[target].built) { /* набор готов — мгновенный свап */ + /* Позицию переносим пропорцией байтов (буферы наборов разного + * размера), а не из сохранённой в g_doc копии — иначе каждый набор + * скачет по своей старой позиции. */ + uint32_t old_off = view_pos(); + uint32_t old_size = file_size; + doc_switch(target); + g_disp_doc = target; + set_encoding(next); + g_doc[target].enc = next; + view_reanchor(map_off(old_off, old_size, file_size)); + } else { /* строим набор лениво */ + doc_save_live(g_active_doc); /* сохранить текущий (позиция и т.п.) */ +#if WITH_RAW + uint32_t raw_save = raw_pos(); + if (VIEW_IS_RAW(g_view)) /* HEX показывает оригинал — его позиция инвариантна */ + raw_reanchor(0); /* прогрессивный показ нового буфера — с начала */ + g_ready = 0; + render_menu(); /* идёт индексация — прячем F2 */ +#endif + spinner_show(1); + int rc = build_doc(target, next, 1); /* visible: прогрессивный показ первого экрана + * (progress_tick рисует по текущему виду — MD/RAW) */ + spinner_show(0); +#if WITH_RAW + g_ready = 1; + render_menu(); +#endif + if (rc < 0) { /* нет EMM — откат на текущий набор */ + if (target == DOC_UTF8) + g_utf_failed = 1; + doc_load_live(g_active_doc); +#if WITH_RAW + if (VIEW_IS_RAW(g_view)) + raw_reanchor(raw_save); /* буфер прежний — вернуть позицию RAW */ +#endif + return; + } + g_disp_doc = target; /* build_doc уже выставил active/enc */ + } + clamp_top(); /* привести top_line к новому набору (для MD) */ +} + +/* ================================================================== + * Вывод ошибки + * ================================================================== */ + +static void on_exit(void) { + unload_file(); + pal_reset(PAL_CGA); +} + +int main(int argc, char **argv) +{ + const char *path; + if (argc >= 2) { + path = argv[1]; + } else { + path = "README.MD"; + /* Дефолтный файл отсутствует — выводим подсказку без смены режима экрана. */ + { + int chk = open(path, O_RDONLY); + if (chk < 0) { + // settextmode(TEXT_MODE_80x32); + clrscr_attr(ATTR_TEXT); + cputs("MDView v0.3 -- Markdown Viewer for Sprinter\r\n" \ + "(c) 2026 Petrov A.G.\r\n\r\n" \ + "Usage: mdview2 \r\n" \ + " Displays a Markdown document (CP866/CP1251/KOI8R/UTF8, up to 128 KB).\r\n"); + exit(-1); + } + close(chk); + } + } + + atexit(on_exit); + + /* Копируем путь в static filename[] для отображения в статус-баре. */ + { + uint8_t i = 0; + while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) { + filename[i] = path[i]; + i++; + } + filename[i] = 0; + } + + /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер + * уже во время загрузки файла и построения индекса. */ + settextmode(TEXT_MODE_80x32); + clrscr_attr(ATTR_TEXT); + set_pallete(); + prerender_status(); + render_menu(); + render_full_status(); + render_percent_progress(0); + spinner_show(1); + + int rc = load_file(path); + if (rc < 0) { + spinner_show(0); + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("MDView: cannot load file '"); + cputs(path); + cputs("'\n\rError: "); + switch (rc) { + case -1: cputs("open failed"); break; + case -2: cputs("seek failed"); break; + case -3: cputs("mem_alloc_pages failed"); break; + case -4: cputs("short read"); break; + case -5: cputs("index alloc failed"); break; + case -6: cputs("render-cache dir alloc failed"); break; + } + cputs("\n\r\n\r"); + exit(-1); + } + /* Scratch-страница под ремап-буфер строки (win_rest_remap). Handle + * сохраняем отдельно: физический номер страницы нельзя передавать + * mem_free_block(), а on_exit() обязан вернуть EMM системе. */ + g_scratch_blk = mem_alloc_pages(1); + if (g_scratch_blk == 0) { + spinner_show(0); + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("MDView: scratch page allocation failed\n\r"); + exit(-1); + } + g_scratch_phys = mem_get_page(g_scratch_blk, 0); + + /* Оба набора пока не построены (страховка к инициализатору g_doc). */ + g_doc[DOC_8BIT].built = 0; + g_doc[DOC_UTF8].built = 0; + + /* Снимок исходного 8-битного буфера (его читает набор DOC_8BIT). */ + orig_file_blk = file_blk; orig_file_pages = file_pages; orig_file_size = file_size; + memcpy(orig_file_phys, file_phys, MAX_PAGES); + + /* Детекция кодировки по оригиналу. UTF-8 конвертацию НЕ делаем здесь — + * она дорогая (полный проход) и нужна только для UTF-8 набора, который + * строится через build_doc лениво. Так первый экран рисуется по индексации + * первичного набора без лишнего прохода. */ + uint8_t det = detect_encoding(); + + /* Первичный (показываемый) набор — по детекции; второй строится ЛЕНИВО по + * первому F8 (switch_encoding). Во время сборки 8-битного первичного F8 + * разрешён (ремап 8-бит, см. load_key) → показываем метку F8; для UTF-8 + * первичного переключаться пока некуда → метку прячем. */ + uint8_t prim_enc = det; + uint8_t prim_doc = (prim_enc == ENC_UTF8) ? DOC_UTF8 : DOC_8BIT; + g_f8_enabled = (prim_doc == DOC_8BIT); + render_menu(); + + rc = build_doc(prim_doc, prim_enc, 1); /* первичный — видимый */ + if (rc < 0 && prim_doc == DOC_UTF8) { /* UTF-8 не вышел (нет EMM) → 8-бит */ + prim_doc = DOC_8BIT; prim_enc = ENC_CP866; + rc = build_doc(DOC_8BIT, ENC_CP866, 1); + } + if (rc < 0) { + spinner_show(0); + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("MDView: index/cache alloc failed\n\r\n\r"); + exit(-1); + } + if (g_abort) + exit(0); /* F10/Esc во время загрузки — корректный выход */ + if (n_lines == 0) { + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("mdview: empty file"); + exit(-1); + } + + /* Первичный готов — гасим спиннер, включаем F8 (второй набор дособерётся + * лениво по первому переключению) и рисуем финальный статус с ПОЛНЫМ + * счётчиком строк. */ + spinner_show(0); + g_f8_enabled = 1; +#if WITH_RAW + g_ready = 1; /* документ построен — теперь доступен F2 (RAW) */ +#endif + render_menu(); + clamp_top(); + render_full_status(); + draw_viewport_from_cache(); + + /* Второй набор (UTF-8 при 8-битном первичном и наоборот) готовим фоном + * в паузах между клавишами — к первому F8 он обычно уже построен. */ + bg_build_start(); + + for (;;) { + /* Idle-фон: пока второй набор не достроен, вместо блокирующего getkey + * опрашиваем клавиатуру и тратим паузы на bg_step. Холдаун после + * каждой клавиши: при активном скролле (автоповтор) фон молчит и + * плавность не страдает. После достройки — снова блокирующий getkey. */ + if (g_bg_doc != 0xFF) { + uint16_t idle = 0; + while (!kbhit()) { + if (idle < BG_KEY_HOLDOFF) { + idle++; + continue; + } + bg_step(); + if (g_bg_doc == 0xFF) + break; + } + } + uint16_t k = getkey(); + uint8_t ascii = (uint8_t)(k & 0xFF); + uint8_t scan = (uint8_t)((k >> 8) & 0x7F); + + if (ascii) { + if (ascii == 0x1B) + break; /* клавиша Esc */ + continue; + } + if (scan == KEY_F10) { + break; + } + if (scan == KEY_F1) { + show_help(); + continue; + } /* show_help сам перерисует */ + /* F2 — тумблер RAW ↔ MD (из HEX тоже уводит в MD). + * Рабочий только после постройки документа. */ + if (scan == KEY_F2) { + if (!g_ready) continue; + if (g_view == VIEW_MD) { + g_view = g_raw_last; /* → RAW (последний под-режим) */ + raw_seed_from(top_line); /* позиция из MD */ + raw_screen_init(); + status_encoding(); + render_menu(); + render_raw_status(); + raw_draw(); + } else { + uint32_t off = view_pos(); /* позиция из RAW/HEX */ + if (VIEW_IS_RAW(g_view)) + g_raw_last = g_view; /* запомнить под-режим RAW */ + g_view = VIEW_MD; + top_line = line_at_off(off); + clamp_top(); + render_full_status(); render_menu(); + draw_viewport_from_cache(); + } + continue; + } +#if WITH_HEX + /* F4 — тумблер HEX ↔ прежний вид (MD/RAW). */ + if (scan == KEY_F4) { + if (!g_ready) continue; + uint32_t off = view_pos(); /* позиция текущего вида */ + if (g_view == VIEW_HEX) { /* HEX → прежний вид */ + g_view = g_hex_last; + view_reanchor(off); + if (g_view == VIEW_MD) { + clamp_top(); + render_full_status(); render_menu(); + draw_viewport_from_cache(); + } else { + raw_screen_init(); + status_encoding(); render_menu(); + render_raw_status(); raw_draw(); + } + } else { /* MD/RAW → HEX */ + g_hex_last = g_view; + g_view = VIEW_HEX; + view_reanchor(off); /* active → байты оригинала */ + hex_screen_init(); + status_encoding(); render_menu(); + render_hex_status(); hex_draw(); + } + continue; + } +#endif + /* цикл CP866→CP1251→KOI8R→UTF8→CP866; набор дособерётся лениво */ + if (scan == KEY_F8) { + uint8_t next = (g_encoding == ENC_CP866) ? ENC_CP1251 : + (g_encoding == ENC_CP1251) ? ENC_KOI8R : + (g_encoding == ENC_KOI8R) ? ENC_UTF8 : ENC_CP866; + if (next == ENC_UTF8 && g_utf_failed) + next = ENC_CP866; /* UTF-8 недоступен */ + switch_encoding(next); + if (g_abort) + exit(0); /* Esc/F10 во время ленивой досборки */ + /* Перерисовка — по текущему виду (switch_encoding сам не рисует). */ +#if WITH_HEX + if (g_view == VIEW_HEX) { + status_encoding(); + render_hex_status(); + hex_draw(); /* printable-колонка в новой кодировке */ + continue; + } +#endif +#if WITH_RAW + if (g_view != VIEW_MD) { + status_encoding(); + render_raw_status(); + raw_draw(); + continue; + } +#endif + render_full_status(); + draw_viewport_from_cache(); + continue; + } + switch(g_view) { + case VIEW_MD: + /* MD-режим: F1/F8/F10 здесь, навигация — в md_key() (peer к raw_key). */ + md_key(scan); /* навигация MD (сама рисует+статус); незнакомые игнорит */ + continue; +#if WITH_HEX + case VIEW_HEX: + hex_key(scan); /* навигация HEX (сама рисует+процент) */ + continue; +#endif +#if WITH_RAW + case VIEW_RAW_WRAP: + case VIEW_RAW_HSCROLL: + /* RAW-режим: F3 Wrap/Unwrap, своя навигация; F8 — кодировка; F1/F10. */ + /* Wrap ↔ Unwrap */ + if (scan == KEY_F3) { + g_view = (g_view == VIEW_RAW_WRAP) ? VIEW_RAW_HSCROLL : VIEW_RAW_WRAP; + g_raw_last = g_view; + raw_renorm(); + raw_screen_init(); + render_menu(); + render_raw_status(); + raw_draw(); + continue; + } + raw_key(scan); + continue; +#endif /* WITH_RAW */ + default: + break; + } + } + clrscr_attr(ATTR_TEXT); + exit(0); + return 0; +} diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2.h b/applications/Volkov/tests/fixtures/mdview2/mdview2.h new file mode 100644 index 0000000..1c53d6f --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2.h @@ -0,0 +1,304 @@ +/* + * mdview2.h — общие определения и интерфейс между модулями mdview2. + * + * Монолит mdview2.c расщеплён на ядро + модули: index (парсер), md/raw (виды), + * enc (кодировки), table, help. Здесь — разделяемые константы экрана/атрибутов, + * словари флагов/стилей, тип cache_rec_t, extern-объявления разделяемого + * состояния ядра и API каждого модуля (сгруппированы по секциям ниже). + */ +#ifndef MDVIEW2_H +#define MDVIEW2_H + +#include +#include /* COLOR(), COLOR_* */ +#include "mdview2_conf.h" /* WITH_RAW / WITH_HEX — выбор опциональных модулей */ + +/* ---- Геометрия экрана -------------------------------------------- */ +#define SCREEN_W 80 +#define SCREEN_H 32 +#define VIEW_TOP_ROW 1 +#define VIEW_H 30 /* видимая область: строки 1..30 включительно */ +#define MENU_ROW 31 +#define TAB_STOP 4 +#define HPAN_STEP 8u /* шаг горизонтального сдвига (←/→) */ + +/* ---- Геометрия статус-бара (строка 0): фиксированные поля и разделители ---- */ +#define SPINNER_COL 8 /* слот спиннера загрузки */ +#define DIV1_X 45 /* разделитель │ перед числами диапазона строк */ +#define DIV2_X 71 /* разделитель │ перед процентом */ +/* Числа/метка пишутся с DIVn_X+2; область между разделителями — [DIV1_X+1 .. DIV2_X-1]. */ + +/* ---- Параметры файла и памяти ------------------------------------ */ +#define PAGE_BITS 14u +#define PAGE_SIZE (1u << PAGE_BITS) /* размер EMM-страницы: 16 КБ */ +#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u)) +#define MAX_PAGES 16 /* 16 страниц × 16 КБ = 256 КБ */ +#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) +#define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */ + +/* ---- Палитра атрибутов ------------------------------------------- */ +#define ATTR_RESET COLOR(COLOR_LIGHTGRAY, COLOR_BLACK) +#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) +#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE) +#define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLUE) +#define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) +#define ATTR_TEXT_TITLE4 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) +#define ATTR_TEXT_BOLD COLOR(COLOR_LIGHTRED, COLOR_BLUE) +#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) +#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE) +#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE) +#define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE) +#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE) +#define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE) +#define ATTR_HR COLOR(COLOR_CYAN, COLOR_BLUE) +#define ATTR_BOX COLOR(COLOR_CYAN, COLOR_BLUE) +#define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE) +#define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) +#define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN) +#define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) +#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK) +#define ATTR_WARN COLOR(COLOR_YELLOW, COLOR_RED) /* строка-обрыв (исчерпан кэш/лимит) */ + +/* ---- Атрибуты диалога справки ------------------------------------ */ +#define ATTR_HELP_BG COLOR(COLOR_LIGHTGRAY, COLOR_BLACK) +#define ATTR_HELP_BDR COLOR(COLOR_WHITE, COLOR_BLACK) +#define ATTR_HELP_TIT COLOR(COLOR_YELLOW, COLOR_BLACK) +#define ATTR_HELP_HDR COLOR(COLOR_WHITE, COLOR_BLACK) + +/* ---- Режимы просмотра -------------------------------------------- */ +#define VIEW_MD 0 /* markdown с форматированием (по умолчанию) */ +#define VIEW_RAW_WRAP 1 /* RAW: длинные строки переносятся кратно 80 */ +#define VIEW_RAW_HSCROLL 2 /* RAW: длинные строки в одну, гориз. скролл */ +#define VIEW_HEX 3 /* HEX-дамп оригинального файла */ +#define VIEW_IS_RAW(v) ((v) == VIEW_RAW_WRAP || (v) == VIEW_RAW_HSCROLL) + +/* ---- Кодировки --------------------------------------------------- */ +/* 8-битные (CP866/CP1251/KOI8R) различаются только ремапом глифов [128-255] + * на отрисовке; UTF-8 — отдельный декодированный набор. */ +#define ENC_CP866 0 +#define ENC_CP1251 1 +#define ENC_KOI8R 2 +#define ENC_UTF8 3 +#define ENC_UNSUPPORTED -1 /* UTF16 / UTF32 */ +#define CONV_MARGIN 4096u /* на сколько байт держать UTF-конвертацию впереди индексатора */ + +/* ---- Флаги сегмента индекса (IF_*) — общие для индексатора/кэша/вью ---- */ +#define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */ +#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */ +#define IF_BLANK 0x04u /* визуально пустая строка */ +#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */ +#define IF_HSCROLL 0x10u /* блок горизонтально скроллируется ЦЕЛИКОМ (код/таблица), + * включая строки короче 80 — двигаются как единый блок. + * НЕ ставится на HR и границы fence (им нечего прятать). */ +#define IF_TRUNC_MSG 0x20u /* строка-обрыв: рисуется ВЖИВУЮ фикс. текстом (без контент-кэша); + * cache_rec.reserved несёт код причины (TRUNC_*). */ + +/* ---- Причина обрыва индексации (cache_rec.reserved при IF_TRUNC_MSG) ---- */ +#define TRUNC_CONTENT 1 /* исчерпан контент-кэш (MAX_CACHE_CONTENT_PAGES) */ +#define TRUNC_LINES 2 /* исчерпана ёмкость индекса (max_lines) */ +#define TRUNC_FILE 3 /* файл больше MAX_FILE — прочитаны первые 256 КБ */ + +/* ---- Словарь сегментов/стилей (общий для индексатора и таблиц) ---- */ +/* Тип сегмента-продолжения, передаваемый в emit_seg()/inline_scan(). */ +#define CK_PLAIN 0 +#define CK_QUOTE 1 +#define CK_LIST 2 +#define CK_OTHER 3 + +/* Начальный inline-стиль сегмента (INIT_STYLE_*). */ +#define INIT_STYLE_PLAIN 0x0 +#define INIT_STYLE_BOLD 0x1 +#define INIT_STYLE_ITALIC 0x2 +#define INIT_STYLE_UNDER 0x3 +#define INIT_STYLE_CODE 0x4 +#define INIT_STYLE_STRIKE 0x5 + +/* ---- Таблицы: CP866 light box-drawing (общий для индексатора и модуля) ---- */ +#define TBL_MAX_COLS 16 +#define TBL_ATTR ATTR_BOX +#define TBL_H 0xC4 /* ─ */ +#define TBL_V 0xB3 /* │ */ +#define TBL_TL 0xDA /* ┌ */ +#define TBL_TM 0xC2 /* ┬ */ +#define TBL_TR 0xBF /* ┐ */ +#define TBL_ML 0xC3 /* ├ */ +#define TBL_MM 0xC5 /* ┼ */ +#define TBL_MR 0xB4 /* ┤ */ +#define TBL_BL 0xC0 /* └ */ +#define TBL_BM 0xC1 /* ┴ */ +#define TBL_BR 0xD9 /* ┘ */ + +/* ================================================================== * + * Разделяемые символы mdview2.c, используемые модулем RAW. + * ================================================================== */ +extern uint8_t g_view; /* активный режим просмотра (VIEW_*) */ +extern uint32_t file_size; /* размер активного буфера документа */ +extern const uint8_t *g_remap; /* таблица ремапа [128-255] (0 = нет) */ +extern uint8_t g_scratch_phys; /* scratch EMM-страница для win_rest */ + +char fb(uint32_t p); /* байт активного буфера (W3-маппинг) */ +void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint16_t off); +void fill_row(uint8_t y, uint8_t attr); +uint8_t pct16(uint16_t num, uint16_t den); /* num*100/den (0..100), 16-бит, без __divulong */ +uint32_t seg_off(uint16_t idx); /* offset исходника для логической строки idx */ +uint16_t line_at_off(uint32_t off); /* обратно: последняя строка с seg_off <= off */ + +/* ================================================================== * + * API модуля RAW (mdview2_raw.c). + * ================================================================== */ +void raw_seed_from(uint16_t md_top_line); /* вход в RAW: позиция по top_line */ +uint32_t raw_pos(void); /* текущий байт-offset верха экрана RAW */ +void raw_reanchor(uint32_t off); /* поставить RAW на строку с байтом off */ +void raw_draw(void); /* перерисовать область документа в RAW */ +uint8_t raw_pct(void); /* % прокрутки по байтам (для render_raw_status_numbers) */ +uint8_t raw_key(uint8_t scan); /* навигация RAW; 1 = обработано */ +void raw_screen_init(void); /* очистка экрана с нужным аттрибутом */ +void raw_renorm(void); /* выровнять позицию при смене под-режима (F3) */ + +/* ================================================================== * + * API модуля HEX (mdview2_hex.c) — дамп ОРИГИНАЛЬНОГО файла (F4). + * Позиция hex_pos/hex_reanchor — в байтах ОРИГИНАЛА (orig_file_*); + * конвертацию в/из активного буфера делает ядро (map_off). + * ================================================================== */ +uint32_t hex_pos(void); /* offset верхнего ряда (кратен 16) */ +void hex_reanchor(uint32_t orig_off); /* поставить на ряд с байтом orig_off */ +void hex_draw(void); /* перерисовать область документа */ +uint8_t hex_key(uint8_t scan); /* навигация HEX; 1 = обработано */ +void hex_screen_init(void); /* очистка экрана */ + +/* ================================================================== * + * Запись директории рендер-кэша (одна на видимую строку, РОВНО 8 байт — + * cache_dir_get/put адресуют сдвигом idx<<3). Контент — len пар (char,attr). + * ================================================================== */ +typedef struct cache_rec_s { + uint8_t page; /* EMM-страница рендер-кэша (физический номер) */ + uint16_t off; /* смещение в странице, 0..16383 (байты, не ячейки) */ + uint8_t len; /* длина контента в ЯЧЕЙКАХ (char,attr пар), 0..255; 0 = пустая/HR */ + uint8_t flags; /* копия IF_NOWRAP/IF_CODE/IF_BLANK на момент рендера */ + uint8_t reserved; /* не используется */ + uint8_t pad[2]; /* явный резерв, добивка до 8 байт */ +} cache_rec_t; + +void cache_dir_get(uint16_t idx, cache_rec_t *r); /* читатель директории кэша (ядро) */ +void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr); /* печать строки с атрибутом */ + +/* ================================================================== * + * Состояние навигации/представления (определено в mdview2.c). + * ================================================================== */ +extern uint16_t n_lines; /* всего строк в индексе текущего набора */ +extern uint16_t top_line; /* верхняя видимая строка */ +extern uint8_t viewport_x; /* горизонтальный сдвиг (nowrap-строки) */ +extern char filename[]; /* имя файла для статус-бара */ +extern uint8_t g_loading; /* 1 во время index_lines() */ +extern uint8_t g_ready; /* 1 когда первичный документ построен (F2 RAW) */ +extern uint8_t g_f8_enabled; /* можно ли сейчас переключать кодировку */ + +/* ================================================================== * + * Статус-бар (строка 0), меню (строка 31), спиннер — mdview2_status.c. + * Атомарные части: prerender (фикс. хром, один раз) / encoding (поле кодировки) + * / numbers (диапазон строк + %) / menu / spinner — обновляются по отдельности. + * ================================================================== */ +void prerender_status(void); /* фикс. часть: фон, разделители, MDVIEW, имя файла (1 раз) */ +void status_encoding(void); /* только поле кодировки (col 37) */ +void render_md_status_numbers(void); /* числа MD: диапазон строк + % */ +void render_raw_status(void); /* RAW mode: метка режима */ +void render_hex_status(void); /* HEX mode: метка режима */ +void render_full_status(void); /* encoding + numbers (MD) */ +void render_menu(void); /* строка меню (по смене режима/готовности) */ +uint16_t drawable_lines(void); /* число строк, готовых к показу */ +void spinner_tick(void); /* кадр спиннера загрузки */ +void spinner_show(uint8_t on); /* вкл/выкл спиннер */ +void render_percent_progress(uint8_t pct); /* показать процентный прогресс просмотра */ + +/* ================================================================== * + * Отрисовка области MD-документа и прокрутка (mdview2_md.c). + * ================================================================== */ +void draw_viewport_from_cache(void); /* перерисовать область документа из кэша */ +void clamp_top(void); /* привести top_line к диапазону */ +void md_scroll_up(uint16_t n); +void md_scroll_down(uint16_t n); +void md_scroll_horizon(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */ +uint8_t md_key(uint8_t scan); /* навигация MD (после загрузки); 1 = обработано */ + +/* ================================================================== * + * API модуля справки (mdview2_help.c). + * ================================================================== */ +void show_help(void); /* модальный диалог F1 */ + +/* ================================================================== * + * Парсер/индексатор (mdview2_index.c) и разделяемое с ним состояние. + * Хранилище индекса (index_blk/pages/phys, n_lines/max_lines) — в ядре + * (аллокация/doc-slot), индексатор пишет в него; cache_*-писатели и + * progress_tick ядро экспортирует индексатору (spinner_tick — из status). + * ================================================================== */ +#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда в одной странице */ +#define MAX_CACHE_LINE_LEN 255u /* кап длины контента строки в ячейках (буфер g_cells) */ + +extern uint16_t max_lines; /* ёмкость индекса: index_pages * 2048 */ +extern uint8_t index_blk, index_pages; /* дескриптор EMM-блока индекса */ +extern uint8_t index_phys[]; /* физ. страницы индекса */ +extern uint8_t index_truncated; /* индекс упёрся в ёмкость */ +extern uint8_t g_abort; /* F10/Esc во время загрузки → прервать индексацию */ +extern uint8_t g_file_clamped; /* файл был обрезан до MAX_FILE при загрузке (>256 КБ) */ + +uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off); +void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len); +void cache_dir_put(uint16_t idx, const cache_rec_t *r); +void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */ + +/* Резюмируемая индексация (для фоновой сборки второго набора): begin сбрасывает + * проход, step выполняет до budget итераций (блоков исходника) и возвращает 1, + * когда индекс финализирован. Межшаговое состояние — статики модуля index; + * оно не входит в docset_t (см. bg_step ядра). */ +void index_begin(void); +uint8_t index_step(uint8_t budget); +extern uint8_t g_bg_building; /* 1 = фоновый шаг индексации: не трогать экран (спиннер) */ + +/* ================================================================== * + * Буфер ячеек рендера (mdview2.c) — общий с модулем таблиц. + * Таблицы эмитят свои строки теми же примитивами, что и индексатор. + * ================================================================== */ +extern uint8_t g_ncells; /* ячеек в текущем сегменте */ +void gc_put(char ch, uint8_t attr); /* добавить ячейку в g_cells */ +void gc_fill(char ch, uint8_t attr, uint8_t n); /* n одинаковых ячеек */ +uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, + uint8_t ckind, uint8_t line_style, uint8_t base_attr, + uint8_t nowrap); + +/* ================================================================== * + * API модуля таблиц (mdview2_table.c) — вызывается из index_lines(). + * ================================================================== */ +uint32_t row_end(uint32_t p); /* offset завершающего '\n' (или file_size) */ +uint32_t table_first_cell(uint32_t row_start); /* контент первой ячейки строки */ +uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce); +uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend); +void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right); +void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols); + +/* ================================================================== * + * Кодировки и UTF-8 конвертация (mdview2_enc.c). + * Состояние кодека владеется модулем; build_doc()/index_lines() ядра + * оркестрируют его (привязка UTF-буфера к живому file_*, сборка наборов). + * ================================================================== */ +extern uint8_t g_encoding; /* активная кодировка (ENC_*) */ +extern uint8_t g_utf_building; /* 1 пока UTF-конвертация не дошла до конца исходника */ +extern uint8_t utf_avail; /* 1 = страницы UTF-8 выделены */ +extern uint8_t utf_blk, utf_pages; /* дескриптор EMM-буфера UTF-набора */ +extern uint8_t utf_phys[]; /* физ. страницы UTF-буфера */ +extern uint32_t utf_size; /* размер сконвертированного UTF-потока */ + +/* Ядро-owned состояние, читаемое кодеком (определено в mdview2.c). */ +extern uint8_t cur_page; /* текущая страница файла в W3 (инвалидация маппинга) */ +extern uint8_t orig_file_phys[]; /* физ. страницы снимка исходного файла */ +extern uint8_t orig_file_pages; +extern uint32_t orig_file_size; + +void set_encoding(uint8_t enc); /* активная кодировка + таблица ремапа */ +const char *enc_name(uint8_t enc); /* короткое имя для статус-бара */ +uint8_t detect_encoding(void); /* автоопределение по сэмплу байт */ +void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off); /* вывод среза с ремапом */ +void utf_convert_more(uint32_t target); /* инкрементальная конвертация до target байт */ +uint8_t utf_alloc(void); /* выделить EMM-страницы под UTF-набор; 0 = нет EMM */ +uint8_t utf_cp_glyph(uint32_t cp); /* кодпойнт → один CP866-глиф ('?' без аналога) */ + +#endif /* MDVIEW2_H */ diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_conf.h b/applications/Volkov/tests/fixtures/mdview2/mdview2_conf.h new file mode 100644 index 0000000..330505e --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_conf.h @@ -0,0 +1,16 @@ +/* + * mdview2_conf.h — конфигурация сборки mdview2: какие опциональные модули + * просмотра включать. 1 = включить (код входит в бинарь), 0 = исключить + * полностью (нулевой расход кода/памяти). + * + * Модули обёрнуты в #if WITH_xxx целиком, поэтому файлы можно держать в + * сборке всегда (при 0 они компилируются в пустой объектник) — менять + * нужно только эти define. + */ +#ifndef MDVIEW2_CONF_H +#define MDVIEW2_CONF_H + +#define WITH_RAW 1 /* RAW-просмотр исходника (F2): mdview2_raw.c */ +#define WITH_HEX 1 /* HEX-дамп оригинального файла (F4): mdview2_hex.c */ + +#endif /* MDVIEW2_CONF_H */ diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_enc.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_enc.c new file mode 100644 index 0000000..2bf83dc --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_enc.c @@ -0,0 +1,319 @@ +/* + * mdview2_enc.c — кодировки и конвертация UTF-8 → CP866. + * + * Две связанные подсистемы: + * 1. 8-битные кодировки (CP866/CP1251/KOI8-R) — различаются только ремапом + * старших байтов [128-255] на ОТРИСОВКЕ (g_remap, win_rest_remap). + * Автоопределение — detect_encoding по частоте ходовых букв. + * 2. UTF-8 — отдельный набор: исходник инкрементально декодируется в + * utf_phys[] как чистый CP866 (utf_convert_more), после чего весь + * конвейер индексации/кэша работает по нему без ремапа. + * + * Владеет состоянием кодека (conv-буфер, позиция конвертации, UTF-страницы); + * описание разделяемых символов и API — в mdview2.h. Оркестрацией (привязка + * UTF-буфера к живому file_*, сборка наборов) занимается build_doc() ядра. + */ + +#include +#include /* COLOR()/COLOR_* для ATTR_* в attr_is_content */ +#include /* sprinter_page_w3 */ +#include /* bank_read/bank_write/mem_alloc_pages/mem_get_page */ +#include "mdview2.h" + +/* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */ +uint8_t g_encoding = ENC_CP866; /* активная кодировка */ + +uint8_t utf_blk; +uint8_t utf_pages; +uint8_t utf_phys[MAX_PAGES]; +uint32_t utf_size; +uint8_t utf_avail; /* 1 = страницы UTF-8 выделены */ +uint8_t g_utf_building; /* 1 пока конвертация не дошла до конца исходника */ + +/* Приватное состояние конвертера (наружу не торчит — сброс через + * utf_conv_reset(), чтение оригинала через cv_read). */ +static uint32_t utf_src; /* позиция чтения ОРИГИНАЛА (utf-8) */ +static uint8_t cv_page = 0xFF; /* orig-страница в W3 для cv_read (своя от cur_page) */ + +/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80): + * младшие 128 (ASCII) — identity, в ремапе не участвуют (win_rest_remap + * трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая + * пунктуация; неизвестное → '?' (0x3F). Сгенерированы Python codecs. */ +static const uint8_t cp1251_to_866[128] = { + /* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F, + /* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F, + /* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4, + /* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5, + /* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, +}; +static const uint8_t koi8r_to_866[128] = { + /* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE, + /* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F, + /* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6, + /* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63, + /* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, + /* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA, + /* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, + /* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A, +}; + +/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */ +void set_encoding(uint8_t enc) +{ + g_encoding = enc; + g_remap = (enc == ENC_CP1251) ? cp1251_to_866 : + (enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0; +} + +/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты + * различают 8-битные кодировки по частоте. */ +// static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB}; +// static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB}; +// static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC}; +// static uint8_t in_set10(const uint8_t *s, uint8_t b) +// { +// for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1; +// return 0; +// } + +/* Проверяем только пять самых популярных символов (о е а и н) */ +static const uint8_t common866 [5] = {0xAE,0xA5,0xA0,0xA8,0xAD}; +static const uint8_t common1251[5] = {0xEE,0xE5,0xE0,0xE8,0xED}; +static const uint8_t commonkoi [5] = {0xCF,0xC5,0xC1,0xC9,0xCE}; +static uint8_t in_set10(const uint8_t *s, uint8_t b) +{ + for (uint8_t i = 0; i < 5; i++) if (s[i] == b) return 1; + return 0; +} + +/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса). + * BOM → UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по + * частоте ходовых букв; фолбэк CP866. */ +uint8_t detect_encoding(void) +{ + if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF) + return ENC_UTF8; + + uint32_t n = file_size; + if (n > 1024u) n = 1024u; /* сэмпл: первый 1 КБ — детекции хватает */ + uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0; + uint16_t s866 = 0, s1251 = 0, skoi = 0; + + for (uint32_t p = 0; p < n; p++) { + uint8_t b = (uint8_t)fb(p); + if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; } + has_high = 1; + if (in_set10(common866, b)) s866++; + if (in_set10(common1251, b)) s1251++; + if (in_set10(commonkoi, b)) skoi++; + if (cont) { + if ((b & 0xC0) == 0x80) cont--; + else { utf_ok = 0; cont = 0; } + } + else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; } + else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; } + else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; } + else utf_ok = 0; /* битый лид/одиночный континюэйшн */ + } + /* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только + * если это настоящий EOF; на границе сэмпла (n= s866 && s1251 >= skoi) return ENC_CP1251; + if (skoi >= s866) return ENC_KOI8R; + return ENC_CP866; +} + +const char *enc_name(uint8_t enc) +{ + return (enc == ENC_CP1251) ? "CP1251" : + (enc == ENC_KOI8R) ? "KOI8-R" : + (enc == ENC_UTF8) ? "UTF-8 " : "CP866 "; +} + +/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/ + * маркеры — уже CP866, не трогаем). Различаем по attr. */ +static uint8_t attr_is_content(uint8_t a) +{ + return (uint8_t)(a != ATTR_BOX && a != ATTR_HR && + a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER); +} + +/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через + * g_remap (CP1251/KOI8). Без активной таблицы — прямой win_rest. */ +void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off) +{ + if (!g_remap || w == 0) { + win_rest(row, 0, 1, w, page, off); + return; + } + uint8_t buf[SCREEN_W * 2]; + bank_read(page, off, buf, (uint16_t)w * 2u); + for (uint8_t i = 0; i < w; i++) { + uint8_t ch = buf[(uint16_t)i * 2u]; + if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u])) + buf[(uint16_t)i * 2u] = g_remap[ch - 0x80]; /* таблицы хранят только старшие 128 */ + } + bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u); + win_rest(row, 0, 1, w, g_scratch_phys, 0); +} + +/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */ + +/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских + * (Є є Ї ї Ў ў), которые есть в CP866; пропуски → '?' (0x3F). */ +static const uint8_t utf_cyr_to_866[96] = { + /* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F, + /* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + /* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F, +}; + +/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в + * near-памяти, флаш через один bank_write (не побайтовый swap W3). */ +static uint8_t conv_buf[256]; +static uint16_t conv_n; /* байт в буфере, ещё не сброшено */ +static uint8_t conv_page; /* индекс текущей страницы в utf_phys */ +static uint16_t conv_off; /* уже сброшенное смещение в этой странице */ + +static void conv_flush(void) +{ + if (conv_n) { + bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n); + conv_off = (uint16_t)(conv_off + conv_n); + conv_n = 0; + } +} + +/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает + * границу страницы (256 делит 16384 нацело). */ +static void conv_put(uint8_t b) +{ + if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */ + conv_flush(); + conv_page++; + conv_off = 0; + } + conv_buf[conv_n++] = b; + if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush(); +} + +/* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт. + * Таблица вместо switch (экономия кода); линейный поиск дёшев — вызывается + * лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый + * символ — достаточно дописать одну строку {кодпойнт, байт-CP866}. + * (Символы с НЕ-1:1 заменой, напр. … -> "...", обрабатываются отдельно ниже.) */ +typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t; +static const utf_sym_t utf_sym[] = { + {0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp « » → space < > */ + {0x00B0, 0xF8}, {0x00B7, 0xFA}, /* ° · */ + {0x2013, 0x2D}, {0x2014, 0x2D}, /* – — → '-' */ + {0x2018, 0x27}, {0x2019, 0x27}, /* ‘ ’ → '\'' */ + {0x201C, 0x22}, {0x201D, 0x22}, /* “ ” → '"' */ + {0x2022, 0xF9}, {0x2116, 0xFC}, /* • → ∙ № */ + {0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */ + {0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */ + {0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */ + {0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5}, /* ┬ ┴ ┼ */ + {0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB}, /* ▀ ▄ █ */ + {0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */ + {0x2713, 0xFB}, {0x2714, 0xFB}, /* ✓ ✔ → галка */ + {0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕ ✗ ✘ → 'x' */ +}; +#define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0])) + +/* Кодпойнт → ОДИН CP866-глиф: кириллица по таблице, ходовые символы по + * utf_sym[], прочее → '?'. Общая для конвертера и HEX-printable (F4). */ +uint8_t utf_cp_glyph(uint32_t cp) +{ + if (cp < 0x80) return (uint8_t)cp; + if (cp >= 0x0400 && cp <= 0x045F) return utf_cyr_to_866[cp - 0x0400]; + for (uint8_t i = 0; i < UTF_SYM_N; i++) + if (utf_sym[i].utf8 == (uint16_t)cp) return utf_sym[i].cp866; + return '?'; +} + +/* Кодпойнт → CP866-глиф(ы) в поток конвертера. Спецслучаи с НЕ-1:1 заменой + * (… → "...", BOM → ничего) здесь; остальное через utf_cp_glyph(). */ +static void conv_emit_cp(uint32_t cp) +{ + if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; } /* … → "..." */ + if (cp == 0xFEFF) return; /* BOM/ZWNBSP — выкинуть */ + conv_put(utf_cp_glyph(cp)); +} + +/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит + * в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной + * cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и + * чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */ +static uint8_t cv_read(uint32_t s) +{ + uint8_t pg = (uint8_t)(s >> PAGE_BITS); + if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; } + return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK))); +} + +/* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока + * сконвертированных (flushed) байт меньше target и не достигнут конец + * исходника. По выходу file_size = доступная (сконвертированная) часть; при + * достижении конца фиксирует utf_size и снимает g_utf_building. */ +void utf_convert_more(uint32_t target) +{ + cv_page = 0xFF; /* W3 был на utf (fb индексатора) — пере-смаппим orig */ + uint32_t n = orig_file_size; + while (utf_src < n && + (uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) { + uint8_t b = (uint8_t)cv_read(utf_src++); + if (b < 0x80) { + conv_emit_cp(b); + continue; + } + + uint32_t cp; + uint8_t need; + if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; } + else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; } + else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; } + else { conv_put('?'); continue; } + + uint8_t ok = 1; + for (uint8_t k = 0; k < need; k++) { + if (utf_src >= n) { ok = 0; break; } + uint8_t cb = (uint8_t)cv_read(utf_src); + if ((cb & 0xC0) != 0x80) { ok = 0; break; } + cp = (cp << 6) | (uint32_t)(cb & 0x3F); + utf_src++; + } + if (!ok || need == 3) { conv_put('?'); continue; } + conv_emit_cp(cp); + } + conv_flush(); + file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; /* доступно индексатору */ + if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; } + cur_page = 0xFF; /* W3 был на orig — fb индексатора пере-смаппит utf */ +} + +/* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру) + * и сбрасывает приватное состояние конвертера в начало (conv-буфер, utf_src, + * g_utf_building). Сама конвертация — инкрементальная, через utf_convert_more(). + * Возврат: 1 — успех, 0 — нет EMM. */ +uint8_t utf_alloc(void) +{ + utf_pages = orig_file_pages; + utf_blk = mem_alloc_pages(utf_pages); + if (utf_blk == 0) return 0; + for (uint8_t i = 0; i < utf_pages; i++) + utf_phys[i] = mem_get_page(utf_blk, i); + conv_n = 0; conv_page = 0; conv_off = 0; /* converter reset (вызывается один раз) */ + utf_src = 0; + g_utf_building = 1; + return 1; +} diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_help.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_help.c new file mode 100644 index 0000000..6999f0d --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_help.c @@ -0,0 +1,106 @@ +/* + * mdview2_help.c — диалог справки (F1). + * + * Модальное окно поверх документа: рамка с заголовком, список горячих + * клавиш и поддерживаемых markdown-элементов. Блокирует до нажатия любой + * клавиши, после чего восстанавливает статус-бар, область документа и меню. + * + * Зависимости от ядра (mdview2.c): render_full_status / draw_viewport_from_cache + * / render_menu для восстановления экрана. Текст справки — в CP866. + */ + +#include +#include +#include +#include +#include "mdview2.h" + +/* Геометрия диалога справки (в символьных координатах 80×32). */ +#define HELP_X 8u /* левая граница рамки */ +#define HELP_Y 3u /* верхняя граница рамки */ +#define HELP_W 64u /* ширина рамки (включая │) */ +#define HELP_H 26u /* высота рамки (включая ─) */ + +/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой). + * Строка s в кодировке CP866; остаток до края дополняется пробелами. */ +static void help_line(uint8_t r, const char *s, uint8_t attr) +{ + uint8_t x = HELP_X + 1u; + uint8_t y = HELP_Y + 1u + r; + uint8_t len = (uint8_t)strlen(s); + + if (len > HELP_W - 2u) + len = HELP_W - 2u; + + bios_set_place(y, x); + if (len > 0) + bios_writeattr(s, len, attr); + /* place уже продвинут bios_writeattr на len колонок (verified). */ + if (len < HELP_W - 2u) { + bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len)); + } +} + +void show_help(void) +{ + /* " Помощь " в CP866 (8 байт) */ + static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC "; + uint8_t tlen = 8u; + uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */ + uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft); + /* Верхняя граница рамки с заголовком по центру. + * place продвигается каждым BIOS-вызовом (verified), поэтому + * достаточно одной установки места на всю строку рамки. */ + wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */ + bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u)); + if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft); + if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT); + if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt); + wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */ + + /* Боковые границы (левый и правый │ для каждой строки тела) */ + for (uint8_t r = 1u; r < HELP_H - 1u; r++) { + wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */ + wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */ + } + + /* Нижняя граница рамки */ + wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */ + bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u)); + bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u)); + wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */ + + /* Содержимое (20 внутренних строк) */ + uint8_t r = 0; + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " MDView v1.0 (b3) -- Markdown Viewer for Sprinter", ATTR_HELP_HDR); + help_line(r++, " (c) 2026 \x8F\xA5\xE2\xE0\xAE\xA2 \x80\x2E\x83\x2E", + ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Navigation:", ATTR_HELP_HDR); + help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG); + help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG); + help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG); + help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables/unwrap)", ATTR_HELP_BG); + help_line(r++, " Esc F10 Exit", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Markdown elements:", ATTR_HELP_HDR); + help_line(r++, " # ## ### Headings H1-H6, **bold**, *italic*,", ATTR_HELP_BG); + help_line(r++, " `code`, ``` code ```, ~~strike~~, > quote", ATTR_HELP_BG); + help_line(r++, " - * + 1. 2. Ordered list, |----|----| Tables", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Encoding and View modes:", ATTR_HELP_HDR); + help_line(r++, " F2 RAW Mode", ATTR_HELP_BG); + help_line(r++, " F3 Wrap/Unwrap Mode for RAW View", ATTR_HELP_BG); + help_line(r++, " F4 HEX Mode (dump of original file)", ATTR_HELP_BG); + help_line(r++, " F8 Cycle CP866 / CP1251 / KOI8-R / UTF-8", ATTR_HELP_BG); + help_line(r++, " Auto-detected on open; F8 to override", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " File size: up to 256 KB (EMM). Lines: up to 18432.", ATTR_HELP_BG); + + (void)getkey(); + + render_full_status(); + draw_viewport_from_cache(); + render_menu(); +} diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_hex.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_hex.c new file mode 100644 index 0000000..c1eb72e --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_hex.c @@ -0,0 +1,209 @@ +/* + * mdview2_hex.c — HEX-дамп ОРИГИНАЛЬНОГО файла (F4). + * + * Показывает исходные байты файла (orig_file_phys), а не активный буфер: + * hex-дамп декодированного UTF-набора был бы искажением. Printable-колонка + * интерпретируется ТЕКУЩЕЙ кодировкой: CP866 как есть, CP1251/KOI8 через + * g_remap, UTF-8 — глиф на позиции лид-байта (continuation-байты → '.'). + * + * Формат ряда (79 колонок, один атрибут ATTR_TEXT — как RAW): + * 0x012340 │ XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX │ 16 символов + * + * Ряд выровнен на 16 байт и никогда не пересекает границу EMM-страницы + * (16 делит 16384 нацело) — один bank_read на ряд, fb()/W3-маппинг не нужны. + * Позиция (hex_top) живёт в байтах ОРИГИНАЛА; конвертацию в/из байт-позиции + * активного буфера делает ядро (view_pos/view_reanchor через map_off). + */ + +#include +#include +#include +#include +#include /* bank_read (чтение оригинала по страницам EMM) */ +#include "mdview2.h" + +#if WITH_HEX /* весь модуль опционален (mdview2_conf.h); при 0 пустой объектник */ + +#define HEX_BPR 16u /* байт на ряд */ +#define HEX_SPAN ((uint32_t)(VIEW_H - 1) * HEX_BPR) + +/* Геометрия ряда (колонки). */ +#define HX_OFF 1u /* "0x012340" */ +#define HX_SEP1 10u /* │ */ +#define HX_DUMP 12u /* пары hex-цифр */ +#define HX_SEP2 61u /* │ */ +#define HX_PRINT 63u /* printable */ + +static uint32_t hex_top; /* offset верхнего ряда (кратен 16) */ + +uint32_t hex_pos(void) { return hex_top; } + +/* Максимальный hex_top: последний ряд файла на нижней строке экрана. */ +static uint32_t hex_max_top(void) +{ + uint32_t last = (orig_file_size == 0) ? 0 + : ((orig_file_size - 1u) & ~(uint32_t)(HEX_BPR - 1u)); + return (last > HEX_SPAN) ? (last - HEX_SPAN) : 0; +} + +void hex_reanchor(uint32_t orig_off) +{ + uint32_t mt = hex_max_top(); + orig_off &= ~(uint32_t)(HEX_BPR - 1u); + hex_top = (orig_off > mt) ? mt : orig_off; +} + +void hex_screen_init(void) +{ + bios_clearwin(VIEW_TOP_ROW, 0, VIEW_H, SCREEN_W, ATTR_TEXT); +} + +/* ---- отрисовка ----------------------------------------------------- */ + +static uint8_t hexdig(uint8_t v) +{ + v &= 15u; + return (uint8_t)((v < 10u) ? ('0' + v) : ('A' - 10 + v)); +} + +/* Один байт оригинала (для UTF-lookahead за границей ряда — редкий путь). */ +static uint8_t fbo(uint32_t off) +{ + uint8_t b; + bank_read(orig_file_phys[off >> PAGE_BITS], (uint16_t)(off & PAGE_MASK), &b, 1); + return b; +} + +/* Printable-глиф для n байт ряда b[] (off — offset b[0] в файле) → out[]. */ +static void hex_print_cells(const uint8_t *b, uint32_t off, uint8_t n, uint8_t *out) +{ + for (uint8_t i = 0; i < n; i++) { + uint8_t c = b[i]; + uint8_t g; + if (c < 0x20u) { + g = '.'; + } else if (c < 0x80u || g_encoding != ENC_UTF8) { + g = (uint8_t)((c >= 0x80u && g_remap) ? g_remap[c - 0x80u] : c); + } else if ((c & 0xC0u) == 0x80u) { + g = '.'; /* continuation-байт */ + } else { + /* Лид-байт: собрать кодпойнт (хвост может выйти за ряд → fbo). + * 4-байтовые (за пределами CP866) и битые последовательности → '.'. */ + uint32_t cp; + uint8_t need, ok = 1; + if ((c & 0xE0u) == 0xC0u) { cp = (uint32_t)(c & 0x1Fu); need = 1; } + else if ((c & 0xF0u) == 0xE0u) { cp = (uint32_t)(c & 0x0Fu); need = 2; } + else { out[i] = '.'; continue; } + for (uint8_t k = 1; k <= need; k++) { + uint32_t p = off + i + k; + if (p >= orig_file_size) { ok = 0; break; } + uint8_t cb = (uint8_t)((i + k < n) ? b[i + k] : fbo(p)); + if ((cb & 0xC0u) != 0x80u) { ok = 0; break; } + cp = (cp << 6) | (uint32_t)(cb & 0x3Fu); + } + g = ok ? utf_cp_glyph(cp) : '.'; + if (g < 0x20u) g = '.'; /* overlong мог дать управляющий */ + } + out[i] = g; + } +} + +static uint8_t hex_line[SCREEN_W]; /* собранный ряд (только символы, единый attr) */ + +static void hex_draw_row(uint32_t off, uint8_t row) +{ + uint8_t buf[HEX_BPR]; + uint8_t n = 0; + + memset(hex_line, ' ', SCREEN_W); + if (off < orig_file_size) { + uint32_t rem = orig_file_size - off; + n = (rem >= HEX_BPR) ? (uint8_t)HEX_BPR : (uint8_t)rem; + bank_read(orig_file_phys[off >> PAGE_BITS], (uint16_t)(off & PAGE_MASK), buf, n); + } + if (n) { /* ряды за EOF — пустые */ + hex_line[HX_OFF] = '0'; + hex_line[HX_OFF + 1] = 'x'; + hex_line[HX_OFF + 2] = hexdig((uint8_t)(off >> 20)); + hex_line[HX_OFF + 3] = hexdig((uint8_t)(off >> 16)); + hex_line[HX_OFF + 4] = hexdig((uint8_t)(off >> 12)); + hex_line[HX_OFF + 5] = hexdig((uint8_t)(off >> 8)); + hex_line[HX_OFF + 6] = hexdig((uint8_t)(off >> 4)); + hex_line[HX_OFF + 7] = hexdig((uint8_t)off); + hex_line[HX_SEP1] = TBL_V; + for (uint8_t i = 0; i < n; i++) { + uint8_t x = (uint8_t)(HX_DUMP + i * 3u + ((i >= 8u) ? 1u : 0u)); + hex_line[x] = hexdig((uint8_t)(buf[i] >> 4)); + hex_line[x + 1] = hexdig(buf[i]); + } + hex_line[HX_SEP2] = TBL_V; + hex_print_cells(buf, off, n, hex_line + HX_PRINT); + } + bios_set_place(row, 0); + bios_write_until((const char *)hex_line, SCREEN_W, 0); +} + +void hex_draw(void) +{ + uint32_t off = hex_top; + for (uint8_t r = 0; r < VIEW_H; r++) { + hex_draw_row(off, (uint8_t)(VIEW_TOP_ROW + r)); + off += HEX_BPR; + } +} + +/* ---- статус-бар: % по байтам (как calc_raw_pct) --------------------- */ + +static uint8_t calc_hex_pct(void) +{ + return pct16((uint16_t)(hex_top >> 8), (uint16_t)(orig_file_size >> 8)); +} + +/* ---- навигация (вызывается из главного цикла при g_view == VIEW_HEX) */ + +uint8_t hex_key(uint8_t scan) +{ + uint32_t mt = hex_max_top(); + switch (scan) { + /* Одна строка — аппаратный скролл окна + подрисовка одного ряда + * (как в MD/RAW), без перерисовки всего экрана. */ + case KEY_DOWN: + if (hex_top < mt) { + hex_top += HEX_BPR; + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */ + hex_draw_row(hex_top + HEX_SPAN, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); + } + break; + case KEY_UP: + if (hex_top) { + hex_top -= HEX_BPR; + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */ + hex_draw_row(hex_top, VIEW_TOP_ROW); + } + break; + case KEY_PGDN: { + uint32_t nt = hex_top + (uint32_t)VIEW_H * HEX_BPR; + if (nt > mt) nt = mt; + if (nt != hex_top) { hex_top = nt; hex_draw(); } + break; + } + case KEY_PGUP: { + uint32_t d = (uint32_t)VIEW_H * HEX_BPR; + uint32_t nt = (hex_top >= d) ? hex_top - d : 0; + if (nt != hex_top) { hex_top = nt; hex_draw(); } + break; + } + case KEY_HOME: + if (hex_top) { hex_top = 0; hex_draw(); } + break; + case KEY_END: + if (hex_top != mt) { hex_top = mt; hex_draw(); } + break; + default: + return 0; /* не наша клавиша */ + } + render_percent_progress(calc_hex_pct()); + return 1; +} + +#endif /* WITH_HEX */ diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_index.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_index.c new file mode 100644 index 0000000..2740fbb --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_index.c @@ -0,0 +1,1031 @@ +/* + * mdview2_index.c — парсер/индексатор markdown (сердце приложения). + * + * Единственный проход по исходному файлу (index_begin/index_step): классификация строк + * (classify_line, is_*_raw), сборка абзацев с переносом (scan_join_stream), + * inline-разметка (inline_marker/inline_scan) и эмиссия готовых (char,attr)- + * ячеек в рендер-кэш (gc_put → seg_commit_cells → cache_*). Ведёт индекс + * сегментов в EMM (idx_get/idx_put) и докручивает UTF-конвертацию впереди + * позиции чтения. Разделяемые символы и API — в mdview2.h. + * + * Приватно (не торчит в .h): idx_rec_t, cur_rec/cur_seg_off, буфер ячеек + * g_cells, idx_get/idx_put и все классификаторы/сканеры. + */ + +#include +#include +#include /* COLOR()/COLOR_* для ATTR_* в сканерах */ +#include +#include /* bank_read/bank_write (idx_get/idx_put) */ +#include "mdview2.h" + +static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr); /* forward (нужна в inline-сканере) */ +static uint8_t is_hr_raw(uint32_t p); /* forward (нужна в classify_line) */ + +/* Начало следующей физической строки (байт после '\n', либо file_size). */ +static uint32_t next_line(uint32_t p) +{ + p = row_end(p); + return (p < file_size) ? p + 1 : p; +} + +typedef struct idx_rec_s { + uint32_t off; /* смещение первого байта исходного текста для сегмента */ + uint8_t flags; /* битовые флаги IF_* ниже */ + uint8_t style; /* начальный стиль INIT_STYLE_* в начале сегмента */ + uint8_t pad0; /* добивка до 8 байт (2048 записей на страницу 16 КБ) */ + uint8_t pad1; +} idx_rec_t; + +#define INDEX_REC_SIZE 8u + +static uint32_t cur_seg_off; /* смещение последнего emit_seg (кэш в near-памяти) */ +static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */ + +/* Для проверки границ выделения считаем пробел/таб/конец строки/0 пробельными. */ +static uint8_t ws_or_eol(char c) +{ + return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0); +} + +static uint8_t ws_or_eol_or_delim(char c) +{ + return (uint8_t)(c == ' ' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '/' + || c == '\t' || c == '\n' || c == '\r' || c == 0); +} + +/* Маркер inline-выделения (*, **, _) распознаётся только если + * пробельная граница есть ровно с одной стороны (XOR-правило). + * Это защищает выражения вроде "2 * 3", "2 ** 3" и внутрисловные + * случаи (например COLOR_YELLOW) от ложного форматирования. */ +static uint8_t is_emph_flanked(char prev_ch, char next_ch) +{ + return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch)); +} + +/* Символ можно экранировать обратным слешем (\X): любая печатная ASCII- + * пунктуация (как в CommonMark). \* \_ \` \[ и т.п. → литерал X. */ +static uint8_t is_escapable(char c) +{ + return (uint8_t)(c >= '!' && c <= '~' && + !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))); +} + +/* Типы сегмента CK_* и стили INIT_STYLE_* — в mdview2.h (общие с таблицами). */ + +/* Доступ к индексу в EMM. + * Размер записи 8 байт, поэтому запись не пересекает границу страницы + * и bank_read/bank_write работают с цельным блоком. */ +static void idx_get(uint16_t idx, idx_rec_t *r) +{ + uint8_t page = (uint8_t)(idx >> 11); /* номер страницы: idx / 2048 */ + uint16_t off = (uint16_t)((idx & 2047u) << 3); /* смещение записи: (idx % 2048) * 8 */ + bank_read(index_phys[page], off, r, INDEX_REC_SIZE); +} + +static void idx_put(uint16_t idx, const idx_rec_t *r) +{ + uint8_t page = (uint8_t)(idx >> 11); + uint16_t off = (uint16_t)((idx & 2047u) << 3); + bank_write(index_phys[page], off, r, INDEX_REC_SIZE); +} + +uint32_t seg_off(uint16_t idx) +{ + idx_rec_t r; + if (idx >= n_lines) return file_size; + idx_get(idx, &r); + return r.off; +} + +/* Последняя строка индекса с исходным offset <= off — обратный перевод + * байт-позиции в MD-строку (позиционирование при F2/F8). Оффсеты сегментов + * монотонны, поэтому бинарный поиск (~15 bank-чтений). */ +uint16_t line_at_off(uint32_t off) +{ + uint16_t lo = 0, hi = n_lines; + while (lo < hi) { + uint16_t mid = (uint16_t)((lo + hi) >> 1); + if (seg_off(mid) <= off) lo = (uint16_t)(mid + 1); + else hi = mid; + } + return (lo > 0) ? (uint16_t)(lo - 1) : 0; +} + +/* Установка флагов (маска IF_*) всегда идёт в последний emit_seg (n_lines-1). + * Используется near-копия cur_rec, чтобы не делать обратное чтение из банка. */ +static void set_cur_flags(uint8_t f) { cur_rec.flags |= f; idx_put((uint16_t)(n_lines - 1), &cur_rec); } + +/* ================================================================== + * Индексация строк + * ================================================================== */ + +/* INIT_STYLE_* — в mdview2.h (общие с модулем таблиц). */ + +/* Тип строки, который возвращает classify_line(). */ +#define LK_PLAIN 0 +#define LK_H1 1 +#define LK_H2 2 +#define LK_H3 3 +#define LK_H4 4 /* также используется для H5/H6 */ +#define LK_HR 5 +#define LK_ULIST 6 /* маркер ненумерованного списка */ +#define LK_OLIST 7 /* маркер нумерованного списка */ +#define LK_QUOTE 8 /* маркер цитаты */ + +/* Классифицирует логическую строку. + * - Заголовки: 1..4 символа '#' и затем пробел/таб (H5/H6 сводятся к H4). + * - Горизонтальная линия: только '-', '*' или '_' (одного вида, >=3), + * возможно с пробелами/табами между символами. + * - Ненумерованный список: "- ", "* " или "+ " перед содержимым. + * - Нумерованный список: цифры + '.'/ ')' + пробел. + * - Цитата: '>' с опциональным пробелом. + * - Иначе обычный текст. + * Для заголовков/списков/цитат out_content указывает на первый байт + * содержимого после маркера и завершающего пробела. */ +static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) +{ + if (p_start >= file_size) return LK_PLAIN; + + char c0 = fb(p_start); + + /* Заголовок? */ + if (c0 == '#') { + uint8_t lvl = 0; + uint32_t p = p_start; + while (p < file_size && fb(p) == '#' && lvl < 6) { + lvl++; p++; + } + if (p < file_size) { + char ch = fb(p); + if (ch == ' ' || ch == '\t') { + p++; + *out_content = p; + if (lvl > 4) lvl = 4; + return lvl; /* LK_H1 .. LK_H4 */ + } + } + /* Символ '#' без пробела после него не считается заголовком. */ + } + + /* Горизонтальный разделитель. + * Проверяется до списков, чтобы `- - -` не стало маркером списка. */ + if (is_hr_raw(p_start)) return LK_HR; + + /* Для списков/цитат дополнительно допускаем ведущие пробелы + * (базовая поддержка вложенности). */ + uint32_t lp = p_start; + while (lp < file_size && fb(lp) == ' ') lp++; + char cl = (lp < file_size) ? fb(lp) : 0; + + /* Ненумерованный список? */ + if ((cl == '-' || cl == '*' || cl == '+') && + (lp + 1) < file_size && + fb(lp + 1) == ' ') { + *out_content = lp + 2; + return LK_ULIST; + } + + /* Нумерованный список? */ + if (cl >= '0' && cl <= '9') { + uint32_t p = lp; + while (p < file_size) { + char ch = fb(p); + if (ch < '0' || ch > '9') break; + p++; + } + if (p < file_size) { + char ch = fb(p); + if ((ch == '.' || ch == ')') && + (p + 1) < file_size && fb(p + 1) == ' ') { + *out_content = p + 2; + return LK_OLIST; + } + } + } + + /* Цитата? */ + if (cl == '>') { + uint32_t p = lp + 1; + if (p < file_size && fb(p) == ' ') p++; + *out_content = p; + return LK_QUOTE; + } + + return LK_PLAIN; +} + +/* Возвращает видимую ширину префикса (отступ + маркер) в колонках. */ +static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t content_off) +{ + switch (kind) { + case LK_ULIST: + case LK_QUOTE: + case LK_OLIST: return (uint8_t)(content_off - p_start); + default: return 0; /* plain/headers спец-префикса не имеют */ + } +} + +/* ================== Merge: сборка ячеек на этапе индекс-скана ============== + * Форвард-сканер (scan_join_stream/inline_scan + прямые emit'ы в index_lines) + * собирает готовые (char,attr)-ячейки в g_cells ПО ХОДУ единственного прохода + * по файлу — без отдельного второго скана. На каждом emit_seg ячейки + * ПРЕДЫДУЩего сегмента флашатся в кэш (lag-1: к этому моменту сегмент уже + * полностью просканирован); последний сегмент флашится после цикла index_lines. + * g_ncells_at_space — снимок длины на последнем пробеле для усечения буфера + * при переносе строки (отбрасываем уехавшее за границу слово). */ +static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2]; +uint8_t g_ncells; /* ячеек в текущем сегменте (общий с таблицами) */ +static uint8_t g_ncells_at_space; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */ +static uint8_t g_skip_flush; /* следующий emit_seg пропустит lag-флаш (pending уже зафлашен вручную, напр. перед таблицей) */ + +/* Обрыв индексации: причина (0 / TRUNC_CONTENT / TRUNC_LINES) и строка, на + * которой упёрлись. Выставляются ОДИН раз (первая причина побеждает); по ним + * index_lines() в конце пишет строку-сообщение (IF_TRUNC_MSG, рисуется вживую). */ +static uint8_t g_trunc_cause; +static uint16_t g_trunc_line; + +/* Пишет одну (char,attr)-ячейку в g_cells, молча игнорируя переполнение + * MAX_CACHE_LINE_LEN. Оставлена ФУНКЦИЕЙ намеренно: инлайн в сильно + * регистро-нагруженный скан-цикл на Z80/SDCC увеличивает спиллы и замедляет + * (проверено замером — макро-инлайн дал регрессию ~11→13с). */ +void gc_put(char ch, uint8_t attr) +{ + if (g_ncells < MAX_CACHE_LINE_LEN) { + uint16_t i = (uint16_t)g_ncells * 2u; + g_cells[i] = (uint8_t)ch; + g_cells[i + 1] = attr; + g_ncells++; + } +} +void gc_fill(char ch, uint8_t attr, uint8_t n) { while (n-- > 0) gc_put(ch, attr); } + +/* Пишет g_cells как контент кэша для line_idx: flags берём из idx-записи, + * пустой буфер/blank → len 0; иначе резервируем место в пуле и коммитим. */ +static void seg_commit_cells(uint16_t line_idx) +{ + cache_rec_t out_rec; + out_rec.flags = 0; out_rec.reserved = 0; out_rec.pad[0] = 0; out_rec.pad[1] = 0; + idx_rec_t r; + uint8_t have = (uint8_t)(line_idx < n_lines); + if (have) { idx_get(line_idx, &r); out_rec.flags = r.flags; } + if (!have || (r.flags & IF_BLANK) || g_ncells == 0) { + out_rec.page = 0; out_rec.off = 0; out_rec.len = 0; + cache_dir_put(line_idx, &out_rec); + return; + } + out_rec.len = g_ncells; + if (cache_reserve((uint16_t)(g_ncells * 2u), &out_rec.page, &out_rec.off)) { + cache_commit(out_rec.page, out_rec.off, g_cells, (uint16_t)(g_ncells * 2u)); + } else { + /* Контент-кэш исчерпан: фиксируем причину и строку обрыва (один раз). + * index_lines() оборвёт цикл и перезапишет эту строку сообщением. */ + if (!g_trunc_cause) { g_trunc_cause = TRUNC_CONTENT; g_trunc_line = line_idx; } + out_rec.page = 0; out_rec.off = 0; out_rec.len = 0; + } + cache_dir_put(line_idx, &out_rec); +} + + +static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont) +{ + (void)ckind; /* тип сегмента восстанавливается флагами idx-записи */ + if (n_lines >= max_lines) { + index_truncated = 1; + if (!g_trunc_cause) { g_trunc_cause = TRUNC_LINES; g_trunc_line = (uint16_t)(max_lines - 1); } + return; + } + cur_rec.off = off; + cur_rec.flags = (uint8_t)(cont ? IF_CONT : 0); + cur_rec.style = (uint8_t)(style & 7u); + cur_rec.pad0 = 0; + cur_rec.pad1 = 0; + idx_put(n_lines, &cur_rec); + cur_seg_off = off; + n_lines++; + /* Флаш ячеек ПРЕДЫДУЩего сегмента (lag-1): он полностью собран форвард-сканером. */ + if (n_lines >= 2) { + if (g_skip_flush) g_skip_flush = 0; /* pending уже зафлашен вручную */ + else seg_commit_cells((uint16_t)(n_lines - 2)); + } + g_ncells = 0; /* буфер ячеек под новый сегмент */ + g_ncells_at_space = 0; +} + +/* Проверяет, что физическая строка пуста (только пробелы/таб + перевод строки). */ +static uint8_t is_line_blank(uint32_t p) +{ + while (p < file_size) { + char c = fb(p); + if (c == '\n') return 1; + if (c != ' ' && c != '\t') return 0; + p++; + } + return 1; +} + +/* Число ведущих пробелов строки (отступ). Используется для отделения + * dedent-пунктов списка (#3) и для отступа обычного абзаца (#4). */ +static uint8_t leading_spaces(uint32_t p) +{ + uint8_t n = 0; + while (p < file_size && fb(p) == ' ' && n < 250) { n++; p++; } + return n; +} + +/* Быстрая проверка границы fenced-блока по маркеру ``` . + * Ведущие пробелы/табы пропускаются — fence может быть с отступом + * (например, ``` ```c ``` внутри элемента списка с отступом 2+ пробела). + * Язык после бэктиков (```c, ```sh) игнорируется: достаточно трёх ` подряд. */ +static uint8_t is_fence_raw(uint32_t p) +{ + while (p < file_size && (fb(p) == ' ' || fb(p) == '\t')) p++; + if (p + 2 >= file_size) return 0; + return (fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`'); +} + +/* Быстрая проверка горизонтального разделителя (`---`, `***`, `___`). */ +static uint8_t is_hr_raw(uint32_t p) +{ + char c0 = fb(p); + if (c0 != '-' && c0 != '*' && c0 != '_') return 0; + char marker = 0; + uint8_t count = 0, ok = 1; + uint32_t hr_p = p; + while (hr_p < file_size) { + char ch = fb(hr_p++); + if (ch == '\n' || ch == '\r') break; + if (ch == ' ' || ch == '\t') continue; + if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; } + if (marker == 0) marker = ch; + else if (ch != marker) { ok = 0; break; } + count++; + } + return ok && count >= 3; +} + +/* Строка таблицы: первый непустой символ — '|'. + * Ведущие пробелы разрешены; такие строки не склеиваются и не переносятся. */ +static uint8_t is_table_raw(uint32_t p) +{ + while (p < file_size) { + char c = fb(p); + if (c == ' ' || c == '\t') { p++; continue; } + return (uint8_t)(c == '|'); + } + return 0; +} + +/* Разбор одного inline-МАРКЕРА в позиции *pq (< bound): \X, [x], `, **, ~~, + * *, _, \t. Общий код для inline_scan() и scan_join_stream(). Возврат: 1 — + * маркер обработан (вызывающий делает continue), 0 — символ обычный (пробел/ + * текст), его кладёт сам вызывающий (и проверяет перенос). Меняет + * *pq/*pls/*pseg/*pprev. attr берётся как emph_to_attr(line_style, base) — + * для join base=ATTR_TEXT (атрибут «обычного» текста). */ +static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls, + uint8_t *pseg, char *pprev, uint8_t base) +{ + uint32_t q = *pq; + uint8_t ls = *pls; + char ch = fb(q); + uint8_t at = emph_to_attr(ls, base); /* атрибут литералов текущего стиля */ + + if (ls != INIT_STYLE_CODE && ch == '\\' && (q + 1) < bound && is_escapable(fb(q + 1))) { + char nb = fb(q + 1); + gc_put(nb, at); + *pseg += 1; *pq = q + 2; *pprev = nb; return 1; + } + if (ls != INIT_STYLE_CODE && ch == '[' && (q + 2) < bound && fb(q + 2) == ']') { + gc_put('[', ATTR_TEXT_BOLD); gc_put(fb(q + 1), ATTR_TEXT_BOLD); gc_put(']', ATTR_TEXT_BOLD); + *pseg += 3; *pq = q + 3; *pprev = ']'; return 1; + } + if (ch == '`') { + if (ls == INIT_STYLE_PLAIN) { *pls = INIT_STYLE_CODE; *pq = q + 1; return 1; } + if (ls == INIT_STYLE_CODE) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; } + gc_put('`', at); *pseg += 1; *pq = q + 1; *pprev = '`'; return 1; + } + if (ch == '*' && (q + 1) < bound && fb(q + 1) == '*') { + char nx = ((q + 2) < bound) ? fb(q + 2) : '\n'; + if (is_emph_flanked(*pprev, nx)) { + if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_BOLD; *pq = q + 2; return 1; } + if (ls == INIT_STYLE_BOLD && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; } + } + gc_put('*', at); gc_put('*', at); + *pseg += 2; *pq = q + 2; *pprev = '*'; return 1; + } + if (ch == '~' && (q + 1) < bound && fb(q + 1) == '~') { + char nx = ((q + 2) < bound) ? fb(q + 2) : '\n'; + if (is_emph_flanked(*pprev, nx)) { + if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_STRIKE; *pq = q + 2; return 1; } + if (ls == INIT_STYLE_STRIKE && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; } + } + gc_put('~', at); gc_put('~', at); + *pseg += 2; *pq = q + 2; *pprev = '~'; return 1; + } + if (ch == '*' || ch == '_') { + char nx = ((q + 1) < bound) ? fb(q + 1) : '\n'; + if (is_emph_flanked(*pprev, nx)) { + uint8_t ns = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER; + if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = ns; *pq = q + 1; return 1; } + if (ls != INIT_STYLE_PLAIN && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; } + } + gc_put(ch, at); *pseg += 1; *pq = q + 1; *pprev = ch; return 1; + } + if (ch == '\t') { + uint8_t old_sc = *pseg; + uint8_t tgt_full = (uint8_t)((old_sc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); + uint8_t tgt = tgt_full; if (tgt > SCREEN_W) tgt = SCREEN_W; + gc_fill(' ', at, (uint8_t)(tgt_full - old_sc)); + *pseg = tgt; *pq = q + 1; *pprev = ' '; return 1; + } + return 0; /* пробел или обычный символ — обрабатывает вызывающий */ +} + +#define JOIN_MODE_NONE 0u +#define JOIN_MODE_LIST 1u +#define JOIN_MODE_QUOTE 2u +#define JOIN_MODE_PLAIN 3u + +/* ================== Единый сканер сегмента ================================ + * Общий цикл inline-форматирования, переносов и (в join-режимах) склейки + * физических строк — бывшие inline_scan + scan_join_stream. mode: + * JOIN_MODE_NONE — сканировать РОВНО [q, bound) без склейки; '\n' в + * диапазоне не ожидается (вызывающие дают строку/ячейку до EOL); + * активны base_attr (title-attr заголовка) и nowrap (замер таблиц); + * ckind всегда CK_OTHER (continuation-префикс не эмитится). + * JOIN_MODE_LIST/QUOTE/PLAIN — правила '\n'-склейки; bound == file_size, + * base_attr == ATTR_TEXT (emph_to_attr(ls, ATTR_TEXT) тождественно + * бывшему styles_map[ls]), nowrap == 0. + * Возвращает позицию останова; итоговый стиль — через *io_line_style. */ +static uint32_t scan_stream(uint32_t q, uint32_t bound, uint32_t para_start, + uint8_t col, uint8_t ckind, uint8_t *io_line_style, + uint8_t base_attr, uint8_t nowrap, uint8_t mode) +{ + uint32_t last_space = 0xFFFFFFFFu; + uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */ + char prev_ch = ' '; + uint8_t seg_col = col; + uint8_t line_style = *io_line_style; + /* base — атрибут «обычного» текста сегмента. Для заголовка первый сегмент + * имеет title-attr, а continuation-сегменты render рисует как PLAIN — + * переключаем base после 1-го переноса (в join-режимах base == ATTR_TEXT + * и присваивание ничего не меняет). */ + uint8_t base = base_attr; + + while (q < bound && n_lines < max_lines) { + char ch = fb(q); + uint8_t soft_break = 0; + + if (ch == '\n') { + uint32_t next = q + 1; + if (mode == JOIN_MODE_NONE) break; /* NONE: '\n' в [q,bound) не ожидается */ + if (next >= bound) break; + + if (mode == JOIN_MODE_LIST) { + if (is_line_blank(next)) break; + if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; + + { + uint32_t next_content = next; + uint8_t next_kind = classify_line(next, &next_content); + if (next_kind == LK_ULIST || next_kind == LK_OLIST || + next_kind == LK_QUOTE || + (next_kind >= LK_H1 && next_kind <= LK_H4) || + next_kind == LK_HR) { + break; + } + } + while (next < bound) { + char ws = fb(next); + if (ws == ' ' || ws == '\t') next++; + else break; + } + q = next; + soft_break = 1; + ch = ' '; + } else if (mode == JOIN_MODE_QUOTE) { + if (is_line_blank(next)) break; + if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; + + { + uint32_t next_content = next; + uint8_t next_kind = classify_line(next, &next_content); + if (next_kind != LK_QUOTE) break; + + { + uint32_t next_line_end = row_end(next_content); + + { + uint8_t empty_quote = 1; + for (uint32_t z = next_content; z < next_line_end; z++) { + char c = fb(z); + if (c != ' ' && c != '\t') { empty_quote = 0; break; } + } + if (empty_quote) break; + } + + { + uint32_t t = next_content; + while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++; + if (t < next_line_end && fb(t) == '>') break; + } + + while (next_content < next_line_end) { + char ws = fb(next_content); + if (ws == ' ' || ws == '\t') next_content++; + else break; + } + if (next_content < next_line_end && fb(next_content) == '>') { + next_content++; + if (next_content < next_line_end && fb(next_content) == ' ') next_content++; + } + q = next_content; + soft_break = 1; + ch = ' '; + } + } + } else { /* JOIN_MODE_PLAIN */ + if (fb(next) == '\n') break; + if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; + { + uint32_t dummy; + if (classify_line(next, &dummy) != LK_PLAIN) break; + } + if (is_line_blank(next)) break; + + { + uint8_t is_hard = 0; + if (q >= para_start + 1) { + char b1 = fb(q - 1); + if (b1 == '\\') { + is_hard = 1; + } else if (q >= para_start + 2) { + char b2 = fb(q - 2); + if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t')) + is_hard = 1; + } + } + if (is_hard) { + q++; + emit_seg(q, line_style, ckind, 0); + last_space = 0xFFFFFFFFu; + prev_ch = ' '; + seg_col = col; + continue; + } + } + + q++; + /* как render: пропускаем ведущие пробелы/табы строки-продолжения, + * склейка даёт РОВНО один пробел (иначе merge эмитит отступ как лишние ячейки) */ + while (q < bound && (fb(q) == ' ' || fb(q) == '\t')) q++; + soft_break = 1; + ch = ' '; + } + } + + /* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор; при + * soft_break ch==' ' (синтетический пробел склейки) — маркеры + * пропускаем, его кладёт ветка пробела ниже. */ + if (!soft_break && inline_marker(&q, bound, &line_style, &seg_col, &prev_ch, base)) + continue; + + if (ch == ' ') { + gc_put(' ', emph_to_attr(line_style, base)); + if (soft_break) { + last_space = q; /* синтетический пробел склейки */ + } else { + last_space = q + 1; + q++; + } + last_space_style = line_style; + g_ncells_at_space = g_ncells; /* снимок для усечения */ + seg_col++; + prev_ch = ' '; + } else { + gc_put(ch, emph_to_attr(line_style, base)); + seg_col++; + q++; /* soft_break здесь всегда 0 (ch != ' ') */ + prev_ch = ch; + } + if (!nowrap && seg_col >= SCREEN_W) { + uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off); + uint32_t wrap_at = use_ls ? last_space : q; + if (wrap_at >= bound || wrap_at == cur_seg_off) break; + uint8_t wrap_style = use_ls ? last_space_style : line_style; + if (use_ls) g_ncells = g_ncells_at_space; /* усечь до пробела */ + emit_seg(wrap_at, wrap_style, ckind, 1); + base = ATTR_TEXT; /* continuation заголовка → PLAIN (в join base и так ATTR_TEXT) */ + /* Префикс continuation-сегмента: список и отступленный абзац (#4) — + * col пробелов выравнивания; цитата — отступ + маркер 0xB3 + пробел. + * Для обычного абзаца без отступа col==0 → префикса нет; CK_OTHER + * (заголовки/таблицы, NONE-режим) — префикса нет. */ + if (ckind == CK_LIST || ckind == CK_PLAIN) { + gc_fill(' ', ATTR_TEXT, col); + } else if (ckind == CK_QUOTE) { + uint32_t content_off2 = para_start + col; + uint8_t qsp = 0; + while (qsp + 2 < content_off2 && qsp < SCREEN_W && fb(para_start + qsp) == ' ') qsp++; + gc_fill(' ', ATTR_TEXT, qsp); + gc_put(0xB3, ATTR_QUOTE_MARKER); + gc_put(' ', ATTR_TEXT); + } + seg_col = col; + last_space = 0xFFFFFFFFu; + prev_ch = ' '; + q = wrap_at; + /* Откатываем line_style к снимку на момент wrap_at: диапазон + * [wrap_at, q_old) будет пересканирован заново, и переключающие + * символы (обратная кавычка, звёздочка, подчёркивание, тильда) + * внутри него иначе применились бы дважды — именно так возникал + * баг: code-маркер на границе переноса ломает цвет следующего слова (mdview.c, отчёт пользователя + * 2026-06-23). */ + line_style = wrap_style; + } + } + + *io_line_style = line_style; + return q; +} + +/* Тонкая обёртка прежнего API: скан ровно [q, q_end) без склейки строк. + * Вызывается для заголовков и ячеек таблиц (всегда CK_OTHER, col=0). */ +uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, + uint8_t ckind, uint8_t line_style, uint8_t base_attr, + uint8_t nowrap) +{ + (void)scan_stream(q, q_end, q, col, ckind, &line_style, base_attr, nowrap, + JOIN_MODE_NONE); + return line_style; +} + +/* Потоковый сканер для index_lines(): mode задаёт правила '\n'-склейки. */ +static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, + uint8_t ckind, uint8_t *io_line_style, uint8_t mode) +{ + return scan_stream(q, file_size, para_start, col, ckind, io_line_style, + ATTR_TEXT, 0, mode); +} + +/* Отрисовка таблиц (row_end/table_*) — в модуле mdview2_table.c. */ + +/* ================== Резюмируемый проход индексации ======================= + * Бывший монолит index_lines() порезан на begin/step/finish, чтобы фоновая + * сборка второго набора (bg_step ядра) могла выполнять индексацию порциями + * в паузах между клавишами. Всё межшаговое состояние — в статиках модуля + * (idx_p/idx_in_block ниже + g_cells/cur_rec/g_trunc_* выше); оно НЕ входит + * в docset_t и переживает свапы наборов, потому что обработчики клавиш + * видимого набора индексатор не трогают. */ +static uint32_t idx_p; /* позиция чтения исходника между шагами */ +static uint8_t idx_in_block; /* внутри fenced code-блока */ +static uint8_t idx_active; /* 1 = проход начат и ещё не финализирован */ + +/* Начинает новый проход (сброс позиции/буферов/причин обрыва). */ +void index_begin(void) +{ + idx_p = 0; + idx_in_block = 0; + n_lines = 0; + index_truncated = 0; + g_trunc_cause = 0; + g_trunc_line = 0; + cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */ + g_ncells = 0; + g_ncells_at_space = 0; + g_skip_flush = 0; + idx_active = 1; +} + +/* Хвост прохода: финальный флаш ячеек + строка-обрыв (бывший финал index_lines). */ +static void index_finish(void) +{ + idx_active = 0; + /* emit_seg() флашит ячейки с отставанием на одну строку (см. её + * комментарий) — последняя эмитированная строка файла так и остаётся + * незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */ + if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1)); + + /* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по + * `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном + * состоянии (для code-block — один emit_seg на строку, эта ветка не + * достигается). Признак — остановились, а файл не кончился; g_abort и + * исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */ + if (!g_trunc_cause && !g_abort && idx_p < file_size) { + g_trunc_cause = TRUNC_LINES; + g_trunc_line = (uint16_t)(max_lines - 1); + } + + /* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца + * клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку + * (место есть: n_lines < max_lines). Приоритет ниже content/lines — + * те означают, что обрезали ещё раньше, их сообщение точнее. */ + if (!g_trunc_cause && !g_abort && g_file_clamped) { + g_trunc_cause = TRUNC_FILE; + g_trunc_line = n_lines; + } + + /* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort): + * документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение + * рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что + * и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */ + if (g_trunc_cause && !g_abort) { + cache_rec_t r; + r.page = 0; r.off = 0; r.len = 0; + r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause; + r.pad[0] = 0; r.pad[1] = 0; + cache_dir_put(g_trunc_line, &r); + n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */ + } +} + +/* Одна итерация бывшего главного цикла index_lines(): классифицирует и + * эмитит очередной блок исходника (параграф/строку кода/таблицу целиком). + * do{}while(0) сохраняет исходные `continue` тела как выход из итерации + * с записью локальной p обратно в idx_p. */ +static void index_iter(void) +{ + if (g_loading) + progress_tick(); /* первый экран + статус + скролл по готовым данным */ + else if (!g_bg_building && (n_lines & 15) == 0) + spinner_tick(); + + uint32_t p = idx_p; + do { + /* Пустая строка — разделитель параграфов. + * В индекс добавляем не более одной пустой экранной строки подряд. */ + if (is_line_blank(p)) { + p = next_line(p); + /* Серия пустых строк схлопывается в одну визуальную строку. */ + if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) { + emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0); + set_cur_flags(IF_BLANK); + } + continue; + } + + /* ---- fenced code-блок (граница или тело) ---- */ + if (is_fence_raw(p)) { + idx_in_block = !idx_in_block; + emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); + set_cur_flags(IF_NOWRAP); + p = next_line(p); + continue; + } + if (idx_in_block) { + emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); + set_cur_flags(IF_NOWRAP | IF_CODE | IF_HSCROLL); /* код скроллится целым блоком */ + { + uint8_t ccx = 0; + uint32_t pp = p; + while (pp < file_size && fb(pp) != '\n') { + char ch = fb(pp); + if (ch == '\t') { + uint8_t tgt = (uint8_t)((ccx & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); + gc_fill(' ', ATTR_TEXT_CODE, (uint8_t)(tgt - ccx)); + ccx = tgt; + } else if (ch == '\r') { + gc_put(' ', ATTR_TEXT_CODE); ccx++; /* как render: \r → пробел */ + } else { + gc_put(ch, ATTR_TEXT_CODE); ccx++; + } + pp++; + } + } + p = next_line(p); + continue; + } + + /* ---- горизонтальный разделитель ---- */ + if (is_hr_raw(p)) { + emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); + set_cur_flags(IF_NOWRAP); + gc_fill(0xC4, ATTR_HR, SCREEN_W); + p = next_line(p); + continue; + } + + /* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */ + if (is_table_raw(p)) { + /* Освобождаем g_cells под измерение ширин: коммитим pending-сегмент + * сейчас; первый emit_seg таблицы пропустит свой lag-флаш. */ + if (n_lines >= 1) { seg_commit_cells((uint16_t)(n_lines - 1)); g_skip_flush = 1; } + g_ncells = 0; + + /* Проход 1: границы блока + ОТРЕНДЕРЕННЫЕ ширины колонок (мерим тем же + * inline_scan, что и при отрисовке — невидимые маркеры стиля не считаются). */ + uint8_t widths[TBL_MAX_COLS]; + for (uint8_t i = 0; i < TBL_MAX_COLS; i++) widths[i] = 0; + uint8_t ncols = 0; + uint32_t r = p; + while (r < file_size && is_table_raw(r)) { + uint32_t le = row_end(r); + uint8_t sep = table_is_sep_row(r, le); + uint32_t cp = table_first_cell(r); + uint32_t cs, ce; + uint8_t c = 0; + while (c < TBL_MAX_COLS && table_next_cell(&cp, le, &cs, &ce)) { + if (!sep && ce > cs) { + g_ncells = 0; + (void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1); + if (g_ncells > widths[c]) widths[c] = g_ncells; + } + c++; + } + if (c > ncols) ncols = c; + r = (le < file_size) ? le + 1 : le; + } + g_ncells = 0; /* чисто для верхней рамки */ + uint32_t tbl_end = r; + if (ncols == 0) ncols = 1; + + /* Проход 2: верхняя рамка → строки/разделитель → нижняя рамка. */ + emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); + set_cur_flags(IF_NOWRAP | IF_HSCROLL); + table_border(widths, ncols, TBL_TL, TBL_TM, TBL_TR); + + r = p; + while (r < tbl_end) { + uint32_t le = row_end(r); + emit_seg(r, INIT_STYLE_PLAIN, CK_OTHER, 0); + set_cur_flags(IF_NOWRAP | IF_HSCROLL); + if (table_is_sep_row(r, le)) table_border(widths, ncols, TBL_ML, TBL_MM, TBL_MR); + else table_data_row(r, le, widths, ncols); + r = (le < file_size) ? le + 1 : le; + } + + emit_seg(tbl_end, INIT_STYLE_PLAIN, CK_OTHER, 0); + set_cur_flags(IF_NOWRAP | IF_HSCROLL); + table_border(widths, ncols, TBL_BL, TBL_BM, TBL_BR); + + p = tbl_end; + continue; + } + + /* ---- классификация текущей строки ---- */ + uint32_t content_off = p; + uint8_t kind = classify_line(p, &content_off); + + /* ---- Заголовок: отдельная строка, не склеивается с параграфом ---- */ + if (kind >= LK_H1 && kind <= LK_H4) { + uint8_t hbase; /* title-attr, как в render */ + switch (kind) { + case LK_H1: hbase = ATTR_TEXT_TITLE1; break; + case LK_H2: hbase = ATTR_TEXT_TITLE2; break; + case LK_H3: hbase = ATTR_TEXT_TITLE3; break; + default: hbase = ATTR_TEXT_TITLE4; break; + } + emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); + uint32_t line_end = row_end(content_off); + (void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, hbase, 0); + p = next_line(p); + continue; + } + + /* ---- Пункт списка: может занимать несколько физических строк ---- */ + if (kind == LK_ULIST || kind == LK_OLIST) { + uint8_t col = marker_visible_col(kind, p, content_off); + uint8_t line_style = INIT_STYLE_PLAIN; + uint32_t q = content_off; + + emit_seg(p, line_style, CK_LIST, 0); + /* Префикс ПЕРВОГО сегмента: отступ + маркер + пробел (как render). */ + { + uint32_t qq = p, qmax = p + SCREEN_W; + if (kind == LK_ULIST) { + while (qq + 2 < content_off && qq < qmax && fb(qq) == ' ') qq++; + gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p)); + gc_put(0x07, ATTR_LIST_MARKER); + gc_put(' ', ATTR_TEXT); + } else { + while (qq < content_off && qq < qmax && fb(qq) == ' ') qq++; + gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p)); + while (qq < content_off - 1) { gc_put(fb(qq), ATTR_LIST_MARKER); qq++; } + gc_put(' ', ATTR_TEXT); + } + } + q = scan_join_stream(q, p, col, CK_LIST, &line_style, JOIN_MODE_LIST); + + /* Переходим к началу следующей физической строки после пункта. */ + q = next_line(q); + + /* Одна пустая строка между соседними маркерами списка подавляется. + * Две и более пустых строки оставляют видимый разделитель. */ + if (q < file_size && is_line_blank(q)) { + uint32_t t = q; + uint8_t blanks = 0; + while (t < file_size && is_line_blank(t)) { + t = next_line(t); + blanks++; + // if (blanks >= 2) break; + } + if (blanks == 1 && t < file_size) { + uint32_t d = t; + uint8_t nk = classify_line(t, &d); + if (nk == LK_ULIST || nk == LK_OLIST) { + /* #3: подавляем blank только если следующий пункт того же + * уровня или глубже; при МЕНЬШЕМ отступе (dedent) пустую + * строку оставляем — пункты должны визуально разделиться. */ + if (leading_spaces(t) >= leading_spaces(p)) { + p = t; /* подавляем единственную пустую строку */ + continue; + } + } + } + } + + p = q; + continue; + } + + /* ---- Параграф цитаты: может покрывать несколько физических строк ---- */ + if (kind == LK_QUOTE) { + uint8_t col = marker_visible_col(kind, p, content_off); + uint8_t line_style = INIT_STYLE_PLAIN; + uint32_t q = content_off; + + emit_seg(p, line_style, CK_QUOTE, 0); + /* Префикс ПЕРВОГО сегмента: отступ + маркер 0xB3 + пробел (как render LK_QUOTE). */ + { + uint32_t qq = p, qmax = p + SCREEN_W; + while (qq + 1 < content_off && qq < qmax && fb(qq) == ' ') qq++; + gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p)); + gc_put(0xB3, ATTR_QUOTE_MARKER); + gc_put(' ', ATTR_TEXT); + } + q = scan_join_stream(q, p, col, CK_QUOTE, &line_style, JOIN_MODE_QUOTE); + + p = next_line(q); + continue; + } + + /* ---- Обычный текстовый параграф: склейка через soft-break ---- */ + { + uint8_t line_style = INIT_STYLE_PLAIN; + /* #4: если абзац начинается с отступа — все его перенесённые строки + * получают такой же отступ (как continuation-префикс списка, но без + * маркера). Контент сканируем после отступа. Патологически большой + * отступ игнорируем. */ + uint8_t indent = leading_spaces(p); + if (indent >= SCREEN_W / 2) indent = 0; + + emit_seg(p, line_style, CK_PLAIN, 0); + if (indent) gc_fill(' ', ATTR_TEXT, indent); /* префикс первого сегмента */ + + uint32_t q = p + indent; + q = scan_join_stream(q, p, indent, CK_PLAIN, &line_style, JOIN_MODE_PLAIN); + + if (q >= file_size) { + p = file_size; + } else { + /* q стоит на завершающем newline параграфа. + * Переходим на следующую строку, чтобы внешняя петля её + * корректно переклассифицировала. */ + p = next_line(q); + } + } + } while (0); + idx_p = p; +} + +/* Выполняет до budget итераций индексации. Возврат: 1 — проход завершён + * (индекс финализирован index_finish), 0 — есть ещё работа. + * Конвертация UTF-8 держится впереди позиции чтения ЗДЕСЬ, до проверки + * условия останова: пока исходник не сконвертирован до конца, idx_p не + * может «догнать» file_size и оборвать проход преждевременно. */ +uint8_t index_step(uint8_t budget) +{ + if (!idx_active) return 1; + while (budget-- > 0) { + /* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead + * склейки/классификации ~ один абзац). file_size при этом = доступная + * сконвертированная часть и растёт по ходу. */ + if (g_utf_building && (uint32_t)(idx_p + CONV_MARGIN) > file_size) + utf_convert_more((uint32_t)(idx_p + CONV_MARGIN)); + + if (!(idx_p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause)) { + index_finish(); + return 1; + } + index_iter(); + } + return 0; +} +#define EM_NONE 0 +#define EM_BOLD 1 +#define EM_ITALIC 2 +#define EM_UNDER 3 +#define EM_CODE 4 +#define EM_STRIKE 5 + +/* Преобразует внутреннее состояние emphasis в экранный атрибут. */ +static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr) +{ + switch (emph) { + case EM_BOLD: return ATTR_TEXT_BOLD; + case EM_ITALIC: return ATTR_TEXT_ITALIC; + case EM_UNDER: return ATTR_TEXT_UNDERSORE; + case EM_CODE: return ATTR_TEXT_CODE; + case EM_STRIKE: return ATTR_TEXT_STRIKE; + default: return base_attr; + } +} diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_md.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_md.c new file mode 100644 index 0000000..712d949 --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_md.c @@ -0,0 +1,227 @@ +/* + * mdview2_md.c — представление MD-документа (peer к mdview2_raw.c): отрисовка + * области из рендер-кэша + прокрутка (верт./гориз.) + навигация md_key(). + * Статус-бар, меню и спиннер — в mdview2_status.c (общие для MD/RAW). + * + * Работает поверх готового рендер-кэша (cache_dir_get → cache_rec_t) и + * состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого + * парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша + * через win_rest_remap (ремап 8-битных кодировок — в mdview2_enc.c). + * + * Пара видов: mdview2_md.c (MD, из кэша) ↔ mdview2_raw.c (RAW, из fb()). + */ + +#include +#include /* wrchar/scroll/COLOR */ +#include /* bios_set_place/bios_fillcharattr */ +#include "mdview2.h" + +/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без + * единого обращения к fb()/исходному файлу. Горизонтальный скролл для + * nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2 + * байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся + * строка лежит в одной странице, так что off+vx*2 не пересекает + * границу страницы); не nowrap-строки игнорируют viewport_x, как и + * раньше в живом render_line() (там это называлось effective_vx). */ +static void draw_line_from_cache(uint16_t line_idx, uint8_t row) +{ + cache_rec_t rec; + cache_dir_get(line_idx, &rec); + + /* Строка-обрыв (исчерпан контент-кэш / лимит строк): фикс. текст вживую, + * без обращения к контент-кэшу (см. index_lines финализацию). */ + if (rec.flags & IF_TRUNC_MSG) { + const char *msg; + switch (rec.reserved) { + case TRUNC_LINES: msg = "*** Line limit reached - document truncated ***"; break; + case TRUNC_FILE: msg = "*** File too large - truncated at 256 KB ***"; break; + default: msg = "*** Content cache exhausted - document truncated ***"; break; + } + bios_set_place(row, 0); + bios_fillcharattr(' ', ATTR_WARN, SCREEN_W); + put_str_attr(2, row, msg, ATTR_WARN); + return; + } + + bios_set_place(row, 0); + bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W); + + /* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код, + * таблицы) — по ТИПУ, а не по длине: блок двигается целиком, включая + * строки короче 80 (короткая строка, ушедшая за левый край, просто + * показывает маркер '<' на пустом месте). HR и границы fence не имеют + * IF_HSCROLL и не сдвигаются. */ + uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0; + + if (rec.len > vx) { + uint16_t remaining = (uint16_t)(rec.len - vx); + uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining; + uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u); + win_rest_remap(row, w, rec.page, off); + if (remaining > SCREEN_W) { + wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC); + } + } + if (vx > 0) { + wrchar(0, row, '<', ATTR_TRUNC); + } +} + +/* Перерисовывает все VIEW_H строк окна из кэша — основной путь + * перерисовки видимой области после загрузки файла (Фаза 3-4). */ +void draw_viewport_from_cache(void) +{ + for (uint8_t i = 0; i < VIEW_H; i++) { + draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i)); + } +} + + +/* ================================================================== + * Прокрутка + * ================================================================== */ + +/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */ +void clamp_top(void) +{ + if (n_lines <= VIEW_H) { + top_line = 0; + } else if (top_line > n_lines - VIEW_H) { + top_line = (uint16_t)(n_lines - VIEW_H); + } +} + +/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ +void md_scroll_up(uint16_t n) +{ + uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; + if( new_top_line != top_line) { + top_line = new_top_line; + if (n == 1) { + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); + draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW)); + } else { + draw_viewport_from_cache(); + } + } +} + +/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ +void md_scroll_down(uint16_t n) +{ + uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? + (uint16_t)(top_line + n) : n_lines - VIEW_H; + if( new_top_line != top_line) { + top_line = new_top_line; + if (n == 1) { + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); + draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); + } else { + clamp_top(); + draw_viewport_from_cache(); + } + } +} + +/* Горизонтальный сдвиг (только если в окне есть nowrap-строки). + * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. + * Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже + * посчитанная реальная длина в ячейках после рендера), без единого + * обращения к fb()/seg_off()/исходному файлу. */ +void md_scroll_horizon(int8_t delta) +{ + uint16_t maxw = 0; + for (uint8_t i = 0; i < VIEW_H; i++) { + uint16_t li = (uint16_t)(top_line + i); + if (li >= n_lines) break; + cache_rec_t rec; + cache_dir_get(li, &rec); + if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */ + if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */ + } + if (maxw == 0) return; /* нет строк шире экрана */ + + /* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */ + uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0; + if (over > 248u) over = 248u; + uint8_t max_vx = (uint8_t)over; + + int16_t nx = (int16_t)viewport_x + delta; + if (nx < 0) nx = 0; + if (nx > (int16_t)max_vx) nx = max_vx; + + /* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже + * сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для + * `if (new_vx != viewport_x) viewport_x = new_vx;` такой код: + * ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a + * — т.е. для записи переиспользует регистр A, испорченный вычитанием в + * сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx). + * (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 — ровно + * наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем + * портящий sub с пути записи; сравнение ниже только решает, перерисовывать + * ли экран. */ + uint8_t new_vx = (uint8_t)nx; + uint8_t old_vx = viewport_x; + viewport_x = new_vx; + if (new_vx != old_vx) + draw_viewport_from_cache(); +} + +/* Процент прокрутки (0..100) относительно доступного диапазона. */ +static uint8_t calc_md_pct(void) +{ + uint16_t total = drawable_lines(); + if (total <= VIEW_H) return 100; + return pct16(top_line, (uint16_t)(total - VIEW_H)); +} + +/* ---- навигация MD после загрузки (peer к raw_key) ---------------- */ + +/* Обрабатывает навигационную клавишу в MD-режиме (вызывается из главного + * цикла). Сама перерисовывает область/статус. Возврат: 1 — обработано, + * 0 — не наша клавиша (F1/F8/F10 разбирает main). */ +uint8_t md_key(uint8_t scan) +{ + switch (scan) { + case KEY_UP: + md_scroll_up(1); + break; + case KEY_DOWN: + md_scroll_down(1); + break; + case KEY_LEFT: + md_scroll_horizon(-(int8_t)HPAN_STEP); + break; + case KEY_RIGHT: + md_scroll_horizon(+(int8_t)HPAN_STEP); + break; + case KEY_PGUP: + md_scroll_up(VIEW_H); + break; + case KEY_PGDN: + md_scroll_down(VIEW_H); + break; + case KEY_HOME: + if(top_line != 0 || viewport_x != 0 ) { + top_line = 0; + viewport_x = 0; + draw_viewport_from_cache(); + } + break; + case KEY_END: + uint16_t new_top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0; + if(top_line != new_top_line || viewport_x != 0 ) { + top_line = new_top_line; + viewport_x = 0; + draw_viewport_from_cache(); + } + break; + + default: + return 0; /* не наша клавиша */ + } + + render_percent_progress(calc_md_pct()); + render_md_status_numbers(); + return 1; +} diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_raw.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_raw.c new file mode 100644 index 0000000..051214b --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_raw.c @@ -0,0 +1,308 @@ +/* + * mdview2_raw.c — RAW-просмотр исходного текста (без markdown-форматирования). + * + * Работает по АКТИВНОМУ буферу документа через fb() (8-бит = исходные байты, + * UTF-8 = декодированный в CP866 буфер), ремап ≥0x80 (CP1251/KOI8) — на + * отрисовке. Индекс/кэш markdown не используются: позиция — байт-offset, + * 1 байт = 1 ячейка, \t показываем пробелом. Два режима: + * VIEW_RAW_WRAP — длинные строки переносятся кратно 80 (рвём слова); + * VIEW_RAW_HSCROLL — строка в один ряд, горизонтальный скролл. + * + * Память: 0 доп. EMM; near-буфер строки + переиспользование g_scratch_phys. + */ + +#include +#include +#include +#include +#include "mdview2.h" + +#if WITH_RAW /* весь модуль — опционален (mdview_conf.h); при 0 пустой объектник */ + +static uint32_t raw_top; /* байт-offset верхней строки экрана */ +static uint16_t raw_hpan; /* гориз. сдвиг (только HSCROLL) */ + +/* ---- примитивы навигации по физическим строкам / переносам -------- */ + +static uint32_t raw_line_start(uint32_t off) +{ + while (off && fb(off - 1) != '\n') off--; + return off; +} + +/* WRAP: начало следующего ряда от off (\n завершает строку; иначе +80). */ +static uint32_t raw_next_wrap(uint32_t off) +{ + uint32_t e = off, lim = off + SCREEN_W; + while (e < file_size) { + if (fb(e) == '\n') return e + 1; + e++; + if (e >= lim) { /* прошли 80 без \n */ + if (e < file_size && fb(e) == '\n') return e + 1; /* ровно 80 + \n */ + return e; /* длинная строка → продолжение */ + } + } + return e; /* EOF */ +} + +static uint32_t raw_prev_wrap(uint32_t off) +{ + if (off == 0) return 0; + uint32_t pls = raw_line_start(off - 1); /* off-1 — это \n пред. строки */ + while (pls + SCREEN_W < (off - 1)) pls += SCREEN_W; + return pls; +} + +/* HSCROLL: начало следующей/предыдущей физической строки. */ +static uint32_t raw_next_line(uint32_t off) +{ + while (off < file_size && fb(off) != '\n') off++; + return (off < file_size) ? off + 1 : off; +} + +static uint32_t raw_prev_line(uint32_t off) +{ + if (off == 0) return 0; + return raw_line_start(off - 1); +} + +/* ---- отрисовка ряда (общий near-буфер → g_scratch_phys → win_rest) - */ + +static uint8_t raw_buf[SCREEN_W + 1]; /* только символы (атрибут единый, задаётся при выводе) */ + +/* В RAW атрибут единый (ATTR_TEXT): буфер — только 80 символов, без пар + * (char,attr). Кладём один байт-глиф (\t→пробел, ремап ≥0x80). */ +static void raw_cell(uint8_t i, uint8_t ch) +{ + if (ch >= 0x80 && g_remap) ch = g_remap[ch - 0x80]; + raw_buf[i] = ch; +} +/* Вывод готового 80-символьного буфера в строку экрана одним BIOS-вызовом + * (char-буфер + единый attr — без WINREST/scratch-страницы). */ +static void raw_flush(uint8_t row) +{ + bios_set_place(row, 0); + // bios_writeattr((const char *)raw_buf, SCREEN_W, ATTR_TEXT); + // bios_writeattr_until((const char *)raw_buf, SCREEN_W, ATTR_TEXT, 0); + bios_write_until((const char *)raw_buf, SCREEN_W, 0); +} + +static uint32_t raw_draw_wrap_row(uint32_t off, uint8_t row) +{ + uint8_t i = 0; + uint32_t e = off, lim = off + SCREEN_W; + while (i < SCREEN_W && e < file_size) { + char c = fb(e); + if (c == '\n') { + raw_cell(i, 0); + break; + } + raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c)); + i++; e++; + } + raw_flush(row); + return raw_next_wrap(off); +} + +static uint32_t raw_draw_hscroll_row(uint32_t off, uint8_t row) +{ + uint32_t e = off; + while (e < file_size && fb(e) != '\n') e++; /* e = конец строки */ + uint16_t len = e - off; + + uint8_t i = 0; + uint32_t p = off + raw_hpan; + while (i < SCREEN_W && p < e) { + char c = fb(p); + raw_cell(i, (uint8_t)(c == '\t' ? ' ' : c)); + i++; p++; + } + // raw_buf[i] = 0; + + if (raw_hpan > 0) + raw_buf[0] = '<'; + if (len > (uint16_t)raw_hpan + SCREEN_W) + raw_buf[SCREEN_W - 1] = '>'; + if (i < SCREEN_W) + raw_buf[i] = 0; + raw_flush(row); + return (e < file_size) ? e + 1 : e; +} + +/* Рисует один ряд от off (по режиму) и возвращает offset следующего ряда. */ +static uint32_t raw_draw_one(uint32_t off, uint8_t row) +{ + if (off >= file_size) { fill_row(row, ATTR_TEXT); return off; } + return (g_view == VIEW_RAW_WRAP) ? raw_draw_wrap_row(off, row) + : raw_draw_hscroll_row(off, row); +} + +void raw_draw(void) +{ + uint32_t off = raw_top; + for (uint8_t r = 0; r < VIEW_H; r++) + off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + r)); +} + +/* Прокрутка на одну строку: аппаратный scroll окна документа + отрисовка + * ОДНОЙ новой строки (как в MD-режиме). Смещение нижнего ряда вычисляется + * проходом VIEW_H рядов от raw_top — без отдельного инкрементального счётчика, + * который рассинхронизировался, когда контент кончается в середине экрана + * (короче окна после wrap→unwrap у конца файла). */ +static uint32_t raw_next(uint32_t off) +{ + return (g_view == VIEW_RAW_WRAP) ? raw_next_wrap(off) : raw_next_line(off); +} +static uint32_t raw_prev(uint32_t off) +{ + return (g_view == VIEW_RAW_WRAP) ? raw_prev_wrap(off) : raw_prev_line(off); +} + +/* ---- статус-бар: % по байтам (отрисовка — render_raw_status_numbers + * в mdview2_status.c, читает позицию через этот raw_pct) ---- */ + +/* Процент по байтам без 32-битного деления: масштабируем offset'ы (>>8 → + * ≤1024), дальше 16-битный pct16 (без __divulong). raw_top приватен модулю, + * поэтому % считаем здесь и отдаём готовым числом в статус-модуль. */ +uint8_t calc_raw_pct(void) +{ + uint16_t a = (uint16_t)(raw_top >> 8); + uint16_t b = (uint16_t)(file_size >> 8); + return pct16(a, b); /* den==0 (файл < 1 КБ) → pct16 вернёт 0 */ +} + +/* ---- посев позиции / пересев при смене активного буфера ----------- */ + +uint32_t raw_pos(void) +{ + return raw_top; +} + +/* Поставить RAW на начало строки, содержащей байт off (позиционирование + * при F2 MD→RAW и F8-переносе позиции между наборами). */ +void raw_reanchor(uint32_t off) +{ + if (off > file_size) off = file_size; + raw_top = raw_line_start(off); + raw_hpan = 0; +} + +void raw_seed_from(uint16_t md_top_line) +{ + raw_reanchor(seg_off(md_top_line)); +} + +/* Сброс в начало (клавиша Home). */ +static void raw_home(void) +{ + raw_top = 0; + raw_hpan = 0; +} + +void raw_end(void) +{ + raw_top = file_size; /* отмотать VIEW_H рядов назад */ + for (uint8_t k = 0; k < VIEW_H; k++) + raw_top = raw_prev(raw_top); + raw_hpan = 0; +} + +void raw_screen_init(void) { + bios_clearwin(1, 0, 30, 80, ATTR_TEXT); +} + +/* Нормализация позиции при смене под-режима RAW (F3 Wrap/Unwrap): выровнять + * raw_top на начало физической строки и снять горизонтальный сдвиг. */ +void raw_renorm(void) +{ + raw_top = raw_line_start(raw_top); + raw_hpan = 0; +} + +void raw_scroll_down(uint8_t delta) { + uint32_t off = raw_top; + for (uint8_t r = 0; r < VIEW_H; r++) { + if (off >= file_size) + return; /* экран не заполнен контентом */ + off = raw_next(off); + } + if (off >= file_size) + return; /* контент ровно по экран, ниже пусто */ + + if(delta ==1) { + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); /* содержимое вверх */ + off = raw_draw_one(off, (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); /* новый нижний ряд */ + raw_top = raw_next(raw_top); + } else { + for (uint8_t k = 0; k < delta; k++) { + uint32_t n2 = raw_next(raw_top); + if (n2 >= file_size) + break; + raw_top = n2; + } + raw_draw(); /* PgUp/PgDn/Home/End — полный экран */ + } +} + +void raw_scroll_up(uint8_t delta) { + if (raw_top == 0) + return; + + if (delta == 1) { + raw_top = raw_prev(raw_top); + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); /* содержимое вниз */ + (void)raw_draw_one(raw_top, VIEW_TOP_ROW); + } else { + for (uint8_t k = 0; k < delta; k++) + raw_top = raw_prev(raw_top); + raw_draw(); /* PgUp/PgDn/Home/End — полный экран */ + } +} + +/* ---- навигация (вызывается из главного цикла при g_view != VIEW_MD) - */ + +uint8_t raw_key(uint8_t scan) +{ + switch (scan) { + case KEY_DOWN: + raw_scroll_down(1); + break; + case KEY_UP: + raw_scroll_up(1); + break; + case KEY_PGDN: + raw_scroll_down(VIEW_H); + break; + case KEY_PGUP: + raw_scroll_up(VIEW_H); + break; + case KEY_HOME: + raw_home(); + raw_draw(); + break; + case KEY_END: + raw_end(); + raw_draw(); + break; + case KEY_LEFT: + if (g_view == VIEW_RAW_HSCROLL && raw_hpan) { + raw_hpan = (raw_hpan >= HPAN_STEP) ? (uint16_t)(raw_hpan - HPAN_STEP) : 0; + raw_draw(); + } + break; + case KEY_RIGHT: + if (g_view == VIEW_RAW_HSCROLL && raw_hpan < 248u) { + raw_hpan = (uint16_t)(raw_hpan + HPAN_STEP); + raw_draw(); + } + break; + default: + return 0; /* не наша клавиша */ + } + + // render_raw_status_numbers(); + render_percent_progress(calc_raw_pct()); + return 1; +} + +#endif /* WITH_RAW */ diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_status.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_status.c new file mode 100644 index 0000000..c767fbc --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_status.c @@ -0,0 +1,192 @@ +/* + * mdview2_status.c — статус-бар (строка 0), нижнее меню (строка 31) и спиннер. + * + * Хром бара АТОМИЗИРОВАН — обновляется по частям, без перерисовки всего бара: + * prerender_status() — фон + разделители │ + "MDVIEW" + имя файла (1 раз); + * status_encoding() — только поле кодировки (col 37), по F8; + * render_md_status_numbers() — числа MD: диапазон строк + % (при скролле); + * render_raw_status_numbers()— числа RAW: метка режима + % по байтам; + * render_full_status() — encoding + numbers (MD), при смене набора/режима; + * render_menu() — строка меню, по смене режима/готовности; + * spinner_tick/show — индикатор занятости во время загрузки. + * Оба *_status_numbers() самоочищаются по геометрии разделителей (DIV1_X/DIV2_X + * в mdview2.h) — фикс. хром не перерисовывается. RAW-позицию даёт raw_pct() + * из mdview2_raw.c (raw_top там приватен). + * + * Раскладка строки 0: + * 0 1..6 8 10.. 37..43 45 46..70 71 72..79 + * . MDVIEW spinner filename encod. │ L a-b / total │ pct% + */ + +#include +#include /* dec8/dec16, cputs */ +#include /* textattr/gotoxy/wrchar/COLOR */ +#include +#include /* bios_fillcharattr/bios_write_until */ +#include "mdview2.h" + +/* ---- Спиннер (слот SPINNER_COL в баре) --------------------------- */ +static const char spinner_chars[4] = { '|', '/', '-', '\\' }; +static uint8_t spinner_phase = 0; +static uint8_t spinner_active = 0; + +/* Продвигает спиннер на один кадр (если включён). */ +void spinner_tick(void) +{ + if (!spinner_active) + return; + wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER); + spinner_phase++; +} + +/* Вкл/выкл спиннер; при выключении гасит его позицию. */ +void spinner_show(uint8_t on) +{ + spinner_active = on; + if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR); +} + +/* ---- Вычисления для чисел статус-бара ----------------------------- */ + +/* Число строк, готовых к показу. Во время загрузки последняя эмитированная + * строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */ +uint16_t drawable_lines(void) +{ + if (g_loading) + return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0; + return n_lines; +} + + +/* ---- Атомарные части статус-бара ---------------------------------- */ + +/* Фиксированный хром бара — рисуется ОДИН раз при старте: фон, два + * разделителя │, метка "MDVIEW" и имя файла (они не меняются за сессию). */ +void prerender_status(void) +{ + fill_row(0, ATTR_BAR); + wrchar(DIV1_X, 0, 0xB3, ATTR_BAR); /* │ */ + wrchar(DIV2_X, 0, 0xB3, ATTR_BAR); /* │ */ + put_str_attr(1, 0, "MDVIEW", ATTR_BAR); + put_str_attr(10, 0, filename, ATTR_BAR); +} + +/* Поле текущей кодировки (col 37, 7 ячеек). Меняется только по F8. */ +void status_encoding(void) +{ + textattr(ATTR_BAR); + gotoxy(DIV1_X - 10, 0); + bios_write_until(enc_name(g_encoding), 8, 0); +} + +static uint16_t local_total = UINT_MAX; +static uint16_t local_last = UINT_MAX; +static uint8_t local_loading = UCHAR_MAX; +static uint8_t local_pct = UCHAR_MAX; + +/* Числовая часть (MD): "L a-b / total" между разделителями + "pct%" справа. + * Перезаписывает ТОЛЬКО области между/после разделителей, сами │ и фикс. + * хром не трогает. */ +void render_md_status_numbers(void) +{ + uint16_t total = drawable_lines(); + uint16_t last = top_line + VIEW_H; + if (last > total) + last = total; + + if(local_total != total || local_last != last || local_loading != g_loading) { + local_total = total; local_last = last; local_loading = g_loading; + textattr(ATTR_BAR); + gotoxy(DIV1_X + 2, 0); + bios_fillcharattr(' ', ATTR_BAR, DIV2_X - DIV1_X - 2); /* очистить [DIV1_X+1 .. DIV2_X-1] */ + gotoxy(DIV1_X + 2, 0); + cputs("L "); + dec16(top_line + 1); + cputs("-"); + dec16(last); + cputs(" / "); + dec16(total); + if (g_loading) cputs("..."); /* ещё грузится */ + } + +} + +/* Полное обновление переменной части (MD): кодировка + числа. */ +void render_full_status(void) +{ + local_loading = UCHAR_MAX; + status_encoding(); + render_md_status_numbers(); +} + +void render_percent_progress(uint8_t pct) { + if (local_pct != pct) { + local_pct = pct; + gotoxy(DIV2_X + 2, 0); + bios_fillcharattr(' ', ATTR_BAR, SCREEN_W - DIV2_X - 2); /* очистить [DIV2_X+1 .. конец] */ + gotoxy(DIV2_X + 2, 0); + dec8(pct); + cputs("%"); + } +} + +#if WITH_RAW +/* RAW-вариант числовой части бара (peer к render_md_status_numbers): метка + * режима между разделителями + процент справа. Сам очищает свои области (та же + * геометрия DIVn_X), атомарен — не требует общего fill всего бара. Позиция + * берётся готовым числом из raw_pct() (raw_top приватен mdview2_raw.c). */ +void render_raw_status(void) +{ + // uint8_t pct = raw_pct(); + textattr(ATTR_BAR); + gotoxy(DIV1_X + 1, 0); + bios_writeattr_until(g_view == VIEW_RAW_WRAP ? " RAW wrap" : " RAW pan", + DIV2_X - DIV1_X - 1, ATTR_BAR, 0); /* метка + пад до DIV2_X-1 */ +} +#endif /* WITH_RAW */ + +#if WITH_HEX +/* HEX-вариант числовой части бара (peer к render_raw_status). */ +void render_hex_status(void) +{ + textattr(ATTR_BAR); + gotoxy(DIV1_X + 1, 0); + bios_writeattr_until(" HEX", DIV2_X - DIV1_X - 1, ATTR_BAR, 0); +} +#endif /* WITH_HEX */ + +/* ---- Нижнее меню (строка 31) -------------------------------------- */ + +void render_menu(void) +{ + /* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль + * ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция + * (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст — + * только у задействованных и доступных сейчас. */ + char num[3]; + + fill_row(MENU_ROW, ATTR_MENU_T); + + num[2] = 0; + for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */ + num[0] = ' '; num[1] = (char)('1' + i); + put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K); + } + put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */ + + put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T); +#if WITH_RAW + if (g_ready) /* RAW доступен после постройки документа */ + put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T); + if (VIEW_IS_RAW(g_view)) /* Wrap/Unwrap — только в RAW */ + put_str_attr(2 * 8 + 2, MENU_ROW, + (g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T); +#endif +#if WITH_HEX + if (g_ready) /* HEX доступен после постройки документа */ + put_str_attr(3 * 8 + 2, MENU_ROW, (g_view == VIEW_HEX) ? "Back" : "Hex", ATTR_MENU_T); +#endif + if (g_f8_enabled) /* смена кодировки сейчас возможна */ + put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T); + put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T); +} diff --git a/applications/Volkov/tests/fixtures/mdview2/mdview2_table.c b/applications/Volkov/tests/fixtures/mdview2/mdview2_table.c new file mode 100644 index 0000000..d0d3aa5 --- /dev/null +++ b/applications/Volkov/tests/fixtures/mdview2/mdview2_table.c @@ -0,0 +1,91 @@ +/* + * mdview2_table.c — выровненная отрисовка markdown-таблиц. + * + * Разбор |-разделённых ячеек и эмиссия рамок/строк в общий буфер ячеек + * рендера (gc_put/gc_fill, см. mdview2.h). Вызывается из index_lines() + * ядра при обнаружении таблицы. Геометрия рамок (TBL_n), словарь стилей + * (CK_n, INIT_STYLE_n) и cell-buffer API объявлены в mdview2.h. + */ + +#include +#include /* COLOR() / COLOR_* для атрибутов TBL_ATTR/ATTR_TEXT */ +#include "mdview2.h" + +uint32_t row_end(uint32_t p) +{ + while (p < file_size && fb(p) != '\n') p++; + return p; +} + +/* Начало контента первой ячейки строки (после ведущих пробелов и '|'). */ +uint32_t table_first_cell(uint32_t row_start) +{ + uint32_t p = row_start; + while (p < file_size && fb(p) == ' ') p++; + if (p < file_size && fb(p) == '|') p++; + return p; +} + +/* Следующая ячейка: [*cs,*ce) — обрезанный диапазон; *pp продвигается за '|'. + * 0 — ячеек больше нет (включая хвостовую пустоту после последнего '|'). */ +uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce) +{ + uint32_t p = *pp; + if (p >= lineend) return 0; + uint32_t a = p; + while (p < lineend && fb(p) != '|') p++; + uint32_t b = p; + uint8_t had_pipe = (uint8_t)(p < lineend && fb(p) == '|'); + if (had_pipe) p++; + *pp = p; + while (a < b && fb(a) == ' ') a++; + while (b > a && fb(b - 1) == ' ') b--; + if (!had_pipe && a == b) return 0; + *cs = a; *ce = b; + return 1; +} + +/* Строка-разделитель (|---|:-:|): каждая ячейка непустая и только '-'/':'. */ +uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend) +{ + uint32_t p = table_first_cell(row_start); + uint32_t cs, ce; + uint8_t n = 0; + while (table_next_cell(&p, lineend, &cs, &ce)) { + if (cs >= ce) return 0; + for (uint32_t q = cs; q < ce; q++) { char c = fb(q); if (c != '-' && c != ':') return 0; } + n++; + } + return (uint8_t)(n > 0); +} + +/* Горизонтальная рамка в g_cells: left + (H×(w+2) + mid|right) по колонкам. */ +void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right) +{ + gc_put(left, TBL_ATTR); + for (uint8_t c = 0; c < ncols; c++) { + gc_fill(TBL_H, TBL_ATTR, (uint8_t)(widths[c] + 2)); + gc_put((c == (uint8_t)(ncols - 1)) ? right : mid, TBL_ATTR); + } +} + +/* Строка данных в g_cells: │ <ячейка, добитая до widths[c]> │ … */ +void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols) +{ + uint32_t p = table_first_cell(row_start); + uint32_t cs, ce; + gc_put(TBL_V, TBL_ATTR); + for (uint8_t c = 0; c < ncols; c++) { + uint8_t got = table_next_cell(&p, lineend, &cs, &ce); + gc_put(' ', TBL_ATTR); + uint8_t rendered = 0; + if (got && ce > cs) { + uint8_t before = g_ncells; + (void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1); + rendered = (uint8_t)(g_ncells - before); + } + if (rendered < widths[c]) gc_fill(' ', ATTR_TEXT, (uint8_t)(widths[c] - rendered)); + gc_put(' ', TBL_ATTR); + gc_put(TBL_V, TBL_ATTR); + } +} diff --git a/applications/Volkov/tests/make_p10_rename_hdd.sh b/applications/Volkov/tests/make_p10_rename_hdd.sh index 609dc76..e0a1a75 100755 --- a/applications/Volkov/tests/make_p10_rename_hdd.sh +++ b/applications/Volkov/tests/make_p10_rename_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD с target-пробой и UI-fixture F6 local rename. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p10_rename.chd} @@ -16,6 +17,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p10_rename_fixture.py" create "$TEMP_DIR" -sh "$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +sh "$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$PROBE" "$TEMP_DIR/RENTEST" printf 'P10 rename HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p11_delete_hdd.sh b/applications/Volkov/tests/make_p11_delete_hdd.sh index 4668bc4..bdded54 100755 --- a/applications/Volkov/tests/make_p11_delete_hdd.sh +++ b/applications/Volkov/tests/make_p11_delete_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD с target-пробой и UI-fixture F8 delete. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p11_delete.chd} @@ -16,6 +17,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p11_delete_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$PROBE" "$TEMP_DIR/DELTEST" printf 'P11 delete HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p12_group_copy_hdd.sh b/applications/Volkov/tests/make_p12_group_copy_hdd.sh index 9be049f..6ef3c9b 100755 --- a/applications/Volkov/tests/make_p12_group_copy_hdd.sh +++ b/applications/Volkov/tests/make_p12_group_copy_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD с двумя панелями для группового F5. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p12_group_copy.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p12_group_copy_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/GRSRC" "$TEMP_DIR/GRDST" printf 'P12 group-copy HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p13_group_delete_hdd.sh b/applications/Volkov/tests/make_p13_group_delete_hdd.sh index bf6fda3..9794464 100755 --- a/applications/Volkov/tests/make_p13_group_delete_hdd.sh +++ b/applications/Volkov/tests/make_p13_group_delete_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture группового F8. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p13_group_delete.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p13_group_delete_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/GDEL" printf 'P13 group-delete HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p14_default_colors_hdd.sh b/applications/Volkov/tests/make_p14_default_colors_hdd.sh index a44cbd0..c1dd8c5 100755 --- a/applications/Volkov/tests/make_p14_default_colors_hdd.sh +++ b/applications/Volkov/tests/make_p14_default_colors_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture стандартных цветов имён. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p14_default_colors.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p14_default_colors_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/COLORS" printf 'P14 default-colours HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p15_tree_cancel_hdd.sh b/applications/Volkov/tests/make_p15_tree_cancel_hdd.sh index f997adb..badd603 100755 --- a/applications/Volkov/tests/make_p15_tree_cancel_hdd.sh +++ b/applications/Volkov/tests/make_p15_tree_cancel_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture отмены рекурсивного F5. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p15_tree_cancel.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p15_tree_cancel_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/CSRC" "$TEMP_DIR/CDST" printf 'P15 tree-cancel HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p15_tree_copy_hdd.sh b/applications/Volkov/tests/make_p15_tree_copy_hdd.sh index be221e9..60578e4 100755 --- a/applications/Volkov/tests/make_p15_tree_copy_hdd.sh +++ b/applications/Volkov/tests/make_p15_tree_copy_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture рекурсивного F5 одного каталога. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p15_tree_copy.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p15_tree_copy_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/SRC" "$TEMP_DIR/DST" printf 'P15 tree-copy HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p15_tree_self_hdd.sh b/applications/Volkov/tests/make_p15_tree_self_hdd.sh index eac8626..0e017db 100755 --- a/applications/Volkov/tests/make_p15_tree_self_hdd.sh +++ b/applications/Volkov/tests/make_p15_tree_self_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture target-inside-source guard. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p15_tree_self.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p15_tree_self_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/SRC" printf 'P15 tree-self HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p16_group_tree_copy_hdd.sh b/applications/Volkov/tests/make_p16_group_tree_copy_hdd.sh index 7b37dc5..df21ae0 100755 --- a/applications/Volkov/tests/make_p16_group_tree_copy_hdd.sh +++ b/applications/Volkov/tests/make_p16_group_tree_copy_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture group F5 с двумя directory roots. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p16_group_tree_copy.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p16_group_tree_copy_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/GSRC" "$TEMP_DIR/GDST" printf 'P16 group-tree HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p17_tree_delete_cancel_hdd.sh b/applications/Volkov/tests/make_p17_tree_delete_cancel_hdd.sh index e35acc7..cec1bbf 100755 --- a/applications/Volkov/tests/make_p17_tree_delete_cancel_hdd.sh +++ b/applications/Volkov/tests/make_p17_tree_delete_cancel_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture runtime-cancel рекурсивного группового F8. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p17_tree_delete_cancel.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p17_tree_delete_cancel_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/CDEL" printf 'P17 tree-delete cancel HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p17_tree_delete_hdd.sh b/applications/Volkov/tests/make_p17_tree_delete_hdd.sh index 6b4eb4b..7e2670b 100755 --- a/applications/Volkov/tests/make_p17_tree_delete_hdd.sh +++ b/applications/Volkov/tests/make_p17_tree_delete_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD-fixture рекурсивного группового F8. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p17_tree_delete.chd} @@ -15,6 +16,6 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p17_tree_delete_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$TEMP_DIR/RDEL" printf 'P17 tree-delete HDD ready: %s\n' "$IMAGE" diff --git a/applications/Volkov/tests/make_p18_metadata_hdd.sh b/applications/Volkov/tests/make_p18_metadata_hdd.sh index 5e1bdce..8036cc5 100755 --- a/applications/Volkov/tests/make_p18_metadata_hdd.sh +++ b/applications/Volkov/tests/make_p18_metadata_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD с точными FAT date/time и R/H/S/A у source-файла. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p18_metadata.chd} @@ -18,7 +19,7 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p18_metadata_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$BASE_CHD" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$BASE_CHD" \ "$COMMANDER" "$TEMP_DIR/MSRC" "$TEMP_DIR/MDST" chdman extractraw -i "$BASE_CHD" -o "$RAW" -f >/dev/null diff --git a/applications/Volkov/tests/make_p19_overwrite_hdd.sh b/applications/Volkov/tests/make_p19_overwrite_hdd.sh index 5714dc1..29578f7 100755 --- a/applications/Volkov/tests/make_p19_overwrite_hdd.sh +++ b/applications/Volkov/tests/make_p19_overwrite_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD для target-пробы транзакционного overwrite. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p19_overwrite.chd} @@ -18,7 +19,7 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p19_overwrite_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$BASE_CHD" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$BASE_CHD" \ "$PROBE" "$TEMP_DIR/SOURCE" "$TEMP_DIR/TARGET" chdman extractraw -i "$BASE_CHD" -o "$RAW" -f >/dev/null printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC" diff --git a/applications/Volkov/tests/make_p19_policy_hdd.sh b/applications/Volkov/tests/make_p19_policy_hdd.sh index bab9984..6fc1897 100755 --- a/applications/Volkov/tests/make_p19_policy_hdd.sh +++ b/applications/Volkov/tests/make_p19_policy_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD полного UI-сценария conflict-policy P19. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p19_policy.chd} @@ -18,7 +19,7 @@ cleanup() trap cleanup EXIT HUP INT TERM python3 "$PROJECT_DIR/tests/p19_policy_fixture.py" create "$TEMP_DIR" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$BASE_CHD" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$BASE_CHD" \ "$COMMANDER" "$TEMP_DIR/P19SRC" "$TEMP_DIR/P19DST" chdman extractraw -i "$BASE_CHD" -o "$RAW" -f >/dev/null printf 'drive z: file="%s" partition=1\n' "$RAW" > "$MTC" diff --git a/applications/Volkov/tests/make_p3_stress_floppy.sh b/applications/Volkov/tests/make_p3_stress_floppy.sh index 16b47ed..ba509b3 100644 --- a/applications/Volkov/tests/make_p3_stress_floppy.sh +++ b/applications/Volkov/tests/make_p3_stress_floppy.sh @@ -1,14 +1,15 @@ #!/bin/sh # Создаёт стандартную FAT12-дискету для P3 stress-теста. -# Аргументы: [образ [исполняемый файл]]; по умолчанию — mc.img и Commander. +# Аргументы: [образ [исполняемый файл]]; по умолчанию — локальный образ. set -eu PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -DEFAULT_IMAGE="$PROJECT_DIR/../../mame/v306/IMG/mc.img" +DEFAULT_IMAGE="$PROJECT_DIR/build/media/p3_stress.img" IMAGE=${1:-$DEFAULT_IMAGE} EXECUTABLE=${2:-$PROJECT_DIR/build/sprcmd.exe} TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/sprcmd-p3.XXXXXX") +mkdir -p "$(dirname -- "$IMAGE")" cleanup() { diff --git a/applications/Volkov/tests/make_p3_stress_hdd.sh b/applications/Volkov/tests/make_p3_stress_hdd.sh index 02e20e1..3516eed 100755 --- a/applications/Volkov/tests/make_p3_stress_hdd.sh +++ b/applications/Volkov/tests/make_p3_stress_hdd.sh @@ -3,6 +3,7 @@ # Аргументы: [образ [исполняемый файл]]; по умолчанию — локальный build/HDD. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p3_stress.chd" @@ -44,7 +45,7 @@ done cp "$PROJECT_DIR/tests/p3_data/P3ROOT.TXT" "$FIXTURE_ROOT/P3ROOT.TXT" cp "$PROJECT_DIR/tests/p3_data/ZETA.BIN" "$FIXTURE_ROOT/ZETA.BIN" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" \ "$FIXTURE_ROOT/EMPTY" "$FIXTURE_ROOT/TESTDIR" \ "$FIXTURE_ROOT/MANY" "$FIXTURE_ROOT/BIGDIR" \ diff --git a/applications/Volkov/tests/make_p4_copy_hdd.sh b/applications/Volkov/tests/make_p4_copy_hdd.sh index d736d75..bc5d76a 100755 --- a/applications/Volkov/tests/make_p4_copy_hdd.sh +++ b/applications/Volkov/tests/make_p4_copy_hdd.sh @@ -3,6 +3,7 @@ # Аргументы: [образ [исполняемый файл]]. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p4_copy.chd" @@ -20,7 +21,7 @@ trap cleanup EXIT HUP INT TERM mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p4_fixture.py" create "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$FIXTURE_ROOT/COPYFROM" "$FIXTURE_ROOT/COPYTO" echo "P4 copy HDD ready: $IMAGE (D:, executable: SPRCMD.EXE)" diff --git a/applications/Volkov/tests/make_p4_exec_hdd.sh b/applications/Volkov/tests/make_p4_exec_hdd.sh index 2628cd3..2a76f40 100755 --- a/applications/Volkov/tests/make_p4_exec_hdd.sh +++ b/applications/Volkov/tests/make_p4_exec_hdd.sh @@ -2,6 +2,7 @@ # Создаёт отдельный HDD для EXEC/restore-теста P4.2. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p4_exec.chd" @@ -25,7 +26,7 @@ cp "$PROJECT_DIR/tests/p3_data/P3ROOT.TXT" \ cp "$PROJECT_DIR/tests/p3_data/INNER.TXT" \ "$FIXTURE_ROOT/EXECLEFT/NOTE.TXT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$FIXTURE_ROOT/EXECLEFT" "$FIXTURE_ROOT/EXECRGHT" echo "P4 EXEC HDD ready: $IMAGE (D:, child: EXECLEFT/P4CHILD.EXE)" diff --git a/applications/Volkov/tests/make_p5_copy_faults_hdd.sh b/applications/Volkov/tests/make_p5_copy_faults_hdd.sh index 7d07494..3293a2e 100755 --- a/applications/Volkov/tests/make_p5_copy_faults_hdd.sh +++ b/applications/Volkov/tests/make_p5_copy_faults_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD с отдельной матрицей отказов банкового copy-job. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p5_copy_faults.chd" @@ -19,7 +20,7 @@ trap cleanup EXIT HUP INT TERM mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p5_copy_faults_fixture.py" create "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$FIXTURE_ROOT/SOURCE" "$FIXTURE_ROOT/TARGET" echo "P5 copy-fault HDD ready: $IMAGE" diff --git a/applications/Volkov/tests/make_p5_enospc_hdd.sh b/applications/Volkov/tests/make_p5_enospc_hdd.sh index 0381089..aafc0b6 100755 --- a/applications/Volkov/tests/make_p5_enospc_hdd.sh +++ b/applications/Volkov/tests/make_p5_enospc_hdd.sh @@ -2,6 +2,7 @@ # Создаёт почти полный HDD для реальной ошибки записи ENOSPC. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p5_enospc.chd" @@ -20,7 +21,7 @@ mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p5_enospc_fixture.py" create "$FIXTURE_ROOT" python3 "$PROJECT_DIR/tests/p5_enospc_fixture.py" verify-initial "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$FIXTURE_ROOT/SOURCE" "$FIXTURE_ROOT/TARGET" \ "$FIXTURE_ROOT/FILLER.BIN" diff --git a/applications/Volkov/tests/make_p5_hdd.sh b/applications/Volkov/tests/make_p5_hdd.sh index 9c92d20..3091c01 100755 --- a/applications/Volkov/tests/make_p5_hdd.sh +++ b/applications/Volkov/tests/make_p5_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD для P5: 20 копирований, 100 refresh и cleanup-циклы. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p5_stability.chd" @@ -20,7 +21,7 @@ mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p5_fixture.py" create "$FIXTURE_ROOT" python3 "$PROJECT_DIR/tests/p5_fixture.py" verify-initial "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$FIXTURE_ROOT/SOURCE" "$FIXTURE_ROOT/TARGET" echo "P5 stability HDD ready: $IMAGE" diff --git a/applications/Volkov/tests/make_p5_media_hdd.sh b/applications/Volkov/tests/make_p5_media_hdd.sh index 4cc5186..e5aa06d 100755 --- a/applications/Volkov/tests/make_p5_media_hdd.sh +++ b/applications/Volkov/tests/make_p5_media_hdd.sh @@ -2,6 +2,7 @@ # Создаёт HDD для физического unload/load во время F5. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p5_media.chd" @@ -18,7 +19,7 @@ trap cleanup EXIT HUP INT TERM mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p5_media_fixture.py" create "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$FIXTURE_ROOT/MEDIA" echo "P5 media-change HDD ready: $IMAGE" diff --git a/applications/Volkov/tests/make_p5_panel_edges_hdd.sh b/applications/Volkov/tests/make_p5_panel_edges_hdd.sh index f482b6e..65414a2 100755 --- a/applications/Volkov/tests/make_p5_panel_edges_hdd.sh +++ b/applications/Volkov/tests/make_p5_panel_edges_hdd.sh @@ -2,6 +2,7 @@ # Создаёт каталоги с итоговыми panel.count ровно 27 и 28 (включая `..`). set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p5_panel_edges.chd" @@ -32,7 +33,7 @@ while [ "$i" -lt 27 ]; do i=$((i + 1)) done -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$FIXTURE_ROOT/N27" "$FIXTURE_ROOT/N28" echo "P5 panel-edge HDD ready: N27=26+parent, N28=27+parent" diff --git a/applications/Volkov/tests/make_p5_viewer_hdd.sh b/applications/Volkov/tests/make_p5_viewer_hdd.sh index 87e1115..68f4b3a 100755 --- a/applications/Volkov/tests/make_p5_viewer_hdd.sh +++ b/applications/Volkov/tests/make_p5_viewer_hdd.sh @@ -7,8 +7,8 @@ PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) DEFAULT_IMAGE="$PROJECT_DIR/build/hdd/p5_viewer.chd" IMAGE=${1:-$DEFAULT_IMAGE} COMMANDER=${2:-$PROJECT_DIR/build/sprcmd.exe} -VIEWER=${3:-$PROJECT_DIR/../../examples/mdview2/mdview2.exe} -README=${4:-$PROJECT_DIR/../../examples/mdview2/README.md} +VIEWER=${3:-$PROJECT_DIR/build/external/mdview2.exe} +README=${4:-$PROJECT_DIR/tests/fixtures/mdview2/README.md} TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/sprcmd-viewer.XXXXXX") FIXTURE_ROOT="$TEMP_DIR/VIEWDATA" @@ -22,7 +22,8 @@ mkdir -p "$(dirname -- "$IMAGE")" "$FIXTURE_ROOT/VIEWER" cp "$VIEWER" "$FIXTURE_ROOT/VIEWER/MDVIEW2.EXE" cp "$README" "$FIXTURE_ROOT/VIEWER/README.MD" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$COMMANDER" "$FIXTURE_ROOT/VIEWER" echo "P5 viewer HDD ready: VIEWER/MDVIEW2.EXE + README.MD" diff --git a/applications/Volkov/tests/make_p6_sort_hdd.sh b/applications/Volkov/tests/make_p6_sort_hdd.sh index 82bf14e..fbd8e09 100755 --- a/applications/Volkov/tests/make_p6_sort_hdd.sh +++ b/applications/Volkov/tests/make_p6_sort_hdd.sh @@ -2,6 +2,7 @@ # Собирает HDD с target-регрессией и двумя каталогами UI-сортировки. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p6_sort.chd} @@ -19,7 +20,7 @@ trap cleanup EXIT HUP INT TERM mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p6_sort_fixture.py" create "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$PROBE" "$FIXTURE_ROOT/SORTL" "$FIXTURE_ROOT/SORTR" echo "P6 sort HDD ready: $IMAGE" diff --git a/applications/Volkov/tests/make_p7_select_hdd.sh b/applications/Volkov/tests/make_p7_select_hdd.sh index 5b594d0..6f03e50 100755 --- a/applications/Volkov/tests/make_p7_select_hdd.sh +++ b/applications/Volkov/tests/make_p7_select_hdd.sh @@ -2,6 +2,7 @@ # Собирает HDD target/UI-регрессии выбора записей. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p7_select.chd} @@ -18,6 +19,6 @@ trap cleanup EXIT HUP INT TERM mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p7_select_fixture.py" create "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$PROBE" "$FIXTURE_ROOT/SELECT" echo "P7 selection HDD ready: $IMAGE" diff --git a/applications/Volkov/tests/make_p9_mkdir_hdd.sh b/applications/Volkov/tests/make_p9_mkdir_hdd.sh index f4bcaa1..d5d73ac 100755 --- a/applications/Volkov/tests/make_p9_mkdir_hdd.sh +++ b/applications/Volkov/tests/make_p9_mkdir_hdd.sh @@ -2,6 +2,7 @@ # Собирает HDD target/UI-регрессии F7 mkdir. set -eu +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для упаковки HDD}" PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) IMAGE=${1:-$PROJECT_DIR/build/hdd/p9_mkdir.chd} @@ -18,6 +19,6 @@ trap cleanup EXIT HUP INT TERM mkdir -p "$(dirname -- "$IMAGE")" python3 "$PROJECT_DIR/tests/p9_mkdir_fixture.py" create "$FIXTURE_ROOT" -"$PROJECT_DIR/../../toolchain/make_hdd.sh" "$IMAGE" \ +"$SPRINTER_ROOT/toolchain/make_hdd.sh" "$IMAGE" \ "$EXECUTABLE" "$PROBE" "$FIXTURE_ROOT/MKTEST" echo "P9 mkdir HDD ready: $IMAGE" diff --git a/applications/Volkov/tests/p1_platform/Makefile b/applications/Volkov/tests/p1_platform/Makefile index dac0137..b995b0c 100644 --- a/applications/Volkov/tests/p1_platform/Makefile +++ b/applications/Volkov/tests/p1_platform/Makefile @@ -8,7 +8,6 @@ # Пробы намеренно собираются в small: это изолированный диагностический # инструмент, а не topology будущего Commander. Сборка big проверяется в P2. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p1probe MEMORY := small ALLOCS ?= 3000 @@ -16,7 +15,7 @@ EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) EXTRA_SRCS := p1_sys.c p1_emm.c p1_dir.c p1_screen.c p1_keyboard.c p1_exec_test.c EXTRA_DATA := p1child.exe -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk # EXEC-тест ожидает дочерний EXE рядом с основной пробой. $(EXE): Makefile p1child.exe diff --git a/applications/Volkov/tests/p2_palette/Makefile b/applications/Volkov/tests/p2_palette/Makefile index 1a4956d..6883e0b 100644 --- a/applications/Volkov/tests/p2_palette/Makefile +++ b/applications/Volkov/tests/p2_palette/Makefile @@ -1,11 +1,10 @@ # P2.2 — отдельная проба системных BIOS/ESTEX-вызовов текстовой палитры. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p2palette MEMORY := big ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile diff --git a/applications/Volkov/tests/p2_screen/Makefile b/applications/Volkov/tests/p2_screen/Makefile index faf6d73..9602872 100644 --- a/applications/Volkov/tests/p2_screen/Makefile +++ b/applications/Volkov/tests/p2_screen/Makefile @@ -3,13 +3,12 @@ # Тест использует системные ESTEX-функции WINREST/RDCHAR/SCROLL, не # открывает BIOS-окна и не обращается к текстовой VRAM напрямую. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p2screen MEMORY := big ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) EXTRA_SRCS := p2_platform.c -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile p2_screen.h sc_glyphs.h diff --git a/applications/Volkov/tests/p3_dir_probe/Makefile b/applications/Volkov/tests/p3_dir_probe/Makefile index 8933b44..12f2dfd 100644 --- a/applications/Volkov/tests/p3_dir_probe/Makefile +++ b/applications/Volkov/tests/p3_dir_probe/Makefile @@ -1,11 +1,10 @@ # Диагностическая проба предельного каталога P3. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p3dir MEMORY := small ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile diff --git a/applications/Volkov/tests/p3_enum_matrix/Makefile b/applications/Volkov/tests/p3_enum_matrix/Makefile index 6101edf..6302b04 100644 --- a/applications/Volkov/tests/p3_enum_matrix/Makefile +++ b/applications/Volkov/tests/p3_enum_matrix/Makefile @@ -1,11 +1,10 @@ # Матрица влияния W3/EMM на длинный F_FIRST/F_NEXT. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p3map MEMORY := big ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) --bank 1=bank1_enum.c -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile bank1_enum.c diff --git a/applications/Volkov/tests/p3_scan_probe/Makefile b/applications/Volkov/tests/p3_scan_probe/Makefile index 1568203..2c52f01 100644 --- a/applications/Volkov/tests/p3_scan_probe/Makefile +++ b/applications/Volkov/tests/p3_scan_probe/Makefile @@ -1,6 +1,5 @@ # Полный scan/store/sort большого каталога теми же модулями, что Commander. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p3scan MEMORY := big ALLOCS ?= 3000 @@ -9,6 +8,6 @@ EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -I ../../include \ --bank 1=../../src/sc_sort.c EXTRA_SRCS := ../../src/sc_panel.c ../../src/sc_store.c -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile ../../src/sc_source_fs.c ../../src/sc_sort.c diff --git a/applications/Volkov/tests/p3_store_probe/Makefile b/applications/Volkov/tests/p3_store_probe/Makefile index ede2a05..830850a 100644 --- a/applications/Volkov/tests/p3_store_probe/Makefile +++ b/applications/Volkov/tests/p3_store_probe/Makefile @@ -1,13 +1,12 @@ # Синтетическая проверка границы одной EMM-страницы панели: 640/641. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p3store MEMORY := big ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -I ../../include EXTRA_SRCS := ../../src/sc_store.c -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile ../../src/sc_store.c ../../include/sc_entry.h \ ../../include/sc_panel.h ../../include/sc_store.h diff --git a/applications/Volkov/tests/p4_bank_io_probe/Makefile b/applications/Volkov/tests/p4_bank_io_probe/Makefile index 3d76d9a..a2f2549 100644 --- a/applications/Volkov/tests/p4_bank_io_probe/Makefile +++ b/applications/Volkov/tests/p4_bank_io_probe/Makefile @@ -1,11 +1,10 @@ # P4 — прямой файловый обмен через EMM-страницу в W3. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p4bio MEMORY := big ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk $(EXE): Makefile diff --git a/applications/Volkov/tests/p4_exec_child/Makefile b/applications/Volkov/tests/p4_exec_child/Makefile index dd03f2b..507718a 100644 --- a/applications/Volkov/tests/p4_exec_child/Makefile +++ b/applications/Volkov/tests/p4_exec_child/Makefile @@ -1,9 +1,8 @@ # Дочерняя программа для интеграционного EXEC-теста Commander. -PROJ_ROOT := $(abspath $(CURDIR)/../../../..) EXAMPLE := p4child MEMORY := tiny ALLOCS ?= 3000 EXTRA_FLAGS := --safe --max-allocs $(ALLOCS) -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../../sdk.mk diff --git a/applications/Volkov/tests/run_mame_hdd.sh b/applications/Volkov/tests/run_mame_hdd.sh index 454661c..e852498 100755 --- a/applications/Volkov/tests/run_mame_hdd.sh +++ b/applications/Volkov/tests/run_mame_hdd.sh @@ -1,6 +1,6 @@ #!/bin/sh # Запускает MAME с локальным тестовым CHD как hard2 (D:) и заданным Lua. -# Скрипт не пересобирает образ и не чистит каталог кадров. +# Состояние эмулятора изолировано; каталог кадров задаёт вызывающий. set -eu @@ -10,36 +10,19 @@ if [ "$#" -lt 3 ] || [ "$#" -gt 4 ]; then fi PROJECT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) -MAME_DIR="$PROJECT_DIR/../../mame/v306" +if [ -z "${SPRINTER_ROOT:-}" ] && [ -f "$PROJECT_DIR/../../bin/sprinter-cc" ]; then + SPRINTER_ROOT="$PROJECT_DIR/../.." +fi +if [ -z "${MAME_HOME:-}" ] && [ -f "$PROJECT_DIR/../../mame/v306/mame.arm" ]; then + MAME_HOME="$PROJECT_DIR/../../mame/v306" +fi +: "${SPRINTER_ROOT:?задайте SPRINTER_ROOT для запуска теста}" +export MAME_HOME IMAGE=$(CDPATH= cd -- "$(dirname -- "$1")" && pwd)/$(basename -- "$1") LUA=$(CDPATH= cd -- "$(dirname -- "$2")" && pwd)/$(basename -- "$2") SNAPSHOT_DIR=$(mkdir -p "$3" && CDPATH= cd -- "$3" && pwd) TIMEOUT=${4:-75} -if pgrep -fl 'mame[^ ]* sprinter( |$)' | grep . >/dev/null 2>&1; then - echo "run_mame_hdd.sh: уже запущен MAME sprinter" >&2 - exit 1 -fi - -cd "$MAME_DIR" -# В macOS GNU timeout может отсутствовать; Python обеспечивает тот же лимит. -if command -v timeout >/dev/null 2>&1; then - set -- timeout "$TIMEOUT" -else - set -- python3 -c ' -import subprocess, sys -try: - sys.exit(subprocess.run(sys.argv[2:], timeout=float(sys.argv[1])).returncode) -except subprocess.TimeoutExpired: - print("run_mame_hdd.sh: истёк лимит времени", file=sys.stderr) - sys.exit(124) -' "$TIMEOUT" -fi -exec "$@" ./mame.arm sprinter \ - -skip_gameinfo -video opengl -window -nofilter \ - -beta:wd179x:0 35hd -beta:wd179x:1 35hd \ - -flop1 ./IMG/mc.img -flop2 ./IMG/dss171u.img \ - -isa0 zxbus_adapter -isa0:zxbus_adapter:card neogs \ - -hard1 ./IMG/sp_hdd_sys.chd -hard2 "$IMAGE" \ - -ata2:0 cdrom -cdrom ./IMG/SprinterCD.iso -bios v3.06 \ - -snapshot_directory "$SNAPSHOT_DIR" -autoboot_script "$LUA" +exec python3 "$SPRINTER_ROOT/toolchain/run_sprinter_mame.py" \ + --hdd "$IMAGE" --script "$LUA" --snapshot-dir "$SNAPSHOT_DIR" \ + --timeout "$TIMEOUT" --video opengl diff --git a/docs/project-reorganization.md b/docs/project-reorganization.md index 12400a6..6e059f2 100644 --- a/docs/project-reorganization.md +++ b/docs/project-reorganization.md @@ -1,6 +1,7 @@ # Разделение Sprinter-CC, MAME и приложений -Статус: план, без переноса файлов. Дата: 2026-09-15. +Статус: реализация. Контракт SDK и локальных образов готов; выделение Git +репозиториев и физический перенос выполняются. Дата: 2026-09-15. ## Цель и границы @@ -25,8 +26,9 @@ MAME приложению не нужны. └── VSCode-Sprinter/ # Git-репозиторий расширения VS Code ``` -Имена `C-Compiler`, `Volkov` и `PoP-Archive` здесь рабочие; адреса Git remote -новых репозиториев и окончательные имена фиксируются до миграции. Папка +Имена `C-Compiler`, `Volkov` и `PoP-Archive` пока локальные; адреса Git remote +новых репозиториев будут добавлены после создания доступных для записи URL. +Отсутствие remote не мешает сохранить независимую локальную историю. Папка `Applications/` не становится общим репозиторием: каждый продукт имеет свою историю, релизы, игнорируемые ресурсы и тесты. Физическое расположение рядом удобно, но не входит в контракт сборки. @@ -167,7 +169,7 @@ MAME документируются вместе; extension не копируе | Корневые `Makefile` и `app.mk` собирают examples/MAME и пишут общую floppy/HDD под `mame/v306` | В SDK оставить `make`/`size-check`/собственные тесты; упаковщик принимает выходной путь и MAME-профиль. Примеры собираются в Examples, MAME — в MAME. Удаление корневых целей без замены лишило бы пользователя привычного run: документировать новые команды и при необходимости оставить переходные targets с явной подсказкой. | | `make host-tests` SDK запускает `applications/SprPoP/tests/host` | Тесты игры вызываются из SprPoP по `SPRINTER_ROOT`; SDK тестирует только SDK. При этом общий `testkit` остаётся SDK. | | SprPoP `mame-link` меняет `MAME_HOME/IMG/test_hdd.chd` | `make run` передаёт `build/hdd/sprpop.chd` как `-hard2`; состояние установки MAME не меняется. | -| Volkov `hdd-p5-viewer` собирает `examples/mdview2` | Перенести внутрь Volkov небольшой viewer fixture либо зафиксированный тестовый EXE/исходник с лицензией. Предпочтение — локальный fixture, потому что тест проверяет запуск дочерней программы, а не функциональность mdview2. Проверить, какие свойства реального viewer обязательны, перед заменой. | +| Volkov `hdd-p5-viewer` собирает `examples/mdview2` | Сохранить зафиксированный исходный viewer fixture внутри Volkov с лицензией и исходным revision: сценарий проверяет fullscreen/raw/PgDn/EMM и восстановление Commander после реального EXEC; упрощённая заглушка не покрыла бы эти свойства. Локальный fixture уже добавлен. | | Volkov `tests/run_mame_hdd.sh` и другие сценарии считают MAME `../../mame/v306` | Принять общие переменные MAME и локальный образ, не менять сценарные Lua-файлы без необходимости. | | PoP `roomtest`, `bgtest`, `poc` ищут собственные `toolchain/` и `SDLPoP` через корень SDK | Ввести `POP_ROOT`/`APP_ROOT` внутри PoP и локальные пути к референсам; общие SDK-инструменты оставить по `SPRINTER_ROOT`. Не переносить чужие источники и оригинальные игровые данные в Git. | | Examples Makefiles и `rpgwalk/conv_sprites.py` рассчитаны на `../..` и тулкитский `third_party/` | Подключать SDK по `SPRINTER_ROOT`, resource fixture держать рядом с примером либо получать по зафиксированному рецепту. | diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..1e6abea --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,22 @@ +local.mk +build/ +.sprinter-cc-*/ +.resource-stamps/ +.disk_tmp/ +*.exe +*.asm +*.lst +*.lk +*.ihx +*.noi +*.sym +*.map +*.rel +*.cdb +*.mem +*.rst +__pycache__/ +*.py[cod] +.DS_Store +._* +.vscode/ diff --git a/examples/.python-version b/examples/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/examples/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/examples/LICENSE b/examples/LICENSE new file mode 100644 index 0000000..a738358 --- /dev/null +++ b/examples/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Sprinter C Compiler contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 0000000..455cf6c --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,11 @@ +# Примеры Sprinter-CC: каждый образ и сборка принадлежат своему примеру. +EXAMPLES := balls mdview mdview2 rpgwalk scroll space +.PHONY: all clean $(EXAMPLES) + +all: $(EXAMPLES) + +$(EXAMPLES): + $(MAKE) -C $@ + +clean: + @for project in $(EXAMPLES); do $(MAKE) -C $$project clean || exit 1; done diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..2b8f604 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,22 @@ +# Примеры Sprinter-CC + +Один репозиторий демонстраций SDK: `balls`, `mdview`, `mdview2`, +`rpgwalk`, `scroll`, `space`. Каждая программа собирается отдельно, а её +дискета находится в `build/media/` внутри каталога примера. + +Для сборки укажите путь к установленному SDK: + +```sh +export SPRINTER_ROOT=/путь/к/C-Compiler +make # все примеры +make -C mdview2 # один пример +make -C mdview2 floppy +``` + +Для запуска дополнительно укажите `MAME_HOME` — каталог подготовленной +среды MAME с `mame.arm`, `roms/` и `IMG/`. `MAME_BIN`, `MAME_ROMPATH`, +`MAME_DSS_IMAGE`, `MAME_SYSTEM_HDD_IMAGE`, `MAME_BIOS` переопределяют +отдельные пути/вариант. Персональные значения можно хранить в игнорируемом +`local.mk`; после перемещения репозитория соседство с SDK не требуется. +Исходник RPG-спрайта хранится в `third_party/16x16-RPG-characters` вместе +с лицензией и нужен только `rpgwalk`. diff --git a/examples/balls/Makefile b/examples/balls/Makefile index 09a01ea..c47c1d3 100644 --- a/examples/balls/Makefile +++ b/examples/balls/Makefile @@ -1,4 +1,3 @@ -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := balls EXTRA_FLAGS ?= --gfx 256 -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk diff --git a/examples/mdview/Makefile b/examples/mdview/Makefile index 3865350..67c7ba1 100644 --- a/examples/mdview/Makefile +++ b/examples/mdview/Makefile @@ -3,11 +3,10 @@ # small memory mode: code in W1, data/stack/heap in W2 (32 KB total). # W3 stays free for the file buffer (EMM-mapped). -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := mdview MEMORY := small -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk # ------------------------------------------------------------------ # Образ дискеты: только mdview.exe + README.MD (перекодированный @@ -33,10 +32,10 @@ $(README_DISK): README.MD | $(DISK_TMP) iconv -c -f UTF-8 -t CP866 README.MD > $@ || true floppy: $(EXAMPLE).exe $(README_DISK) - python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK) + $(PYTHON) $(MAKE_DISK) "$(FLOPPY_IMG)" $(EXAMPLE).exe $(README_DISK) @echo @echo "Floppy ready: $(FLOPPY_IMG)" - @echo "Run: cd $(MAME_DIR) && ./run_mame.sh" + @echo "Run: make run MAME_HOME=/путь/к/MAME/runtime" clean: rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP) diff --git a/examples/mdview2/Makefile b/examples/mdview2/Makefile index bae8bfa..9933047 100644 --- a/examples/mdview2/Makefile +++ b/examples/mdview2/Makefile @@ -6,7 +6,6 @@ # small memory mode: code in W1, data/stack/heap in W2 (32 KB total). # W3 stays free for the file buffer (EMM-mapped). -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := mdview2 EXTRA_SRCS := mdview2_raw.c mdview2_hex.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_md.c mdview2_status.c mdview2_index.c MEMORY := small @@ -14,7 +13,7 @@ MEMORY := small # кода на всём приложении (замер 2026-07-04) ценой более долгой компиляции. EXTRA_FLAGS := --max-allocs 100000 -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk # ------------------------------------------------------------------ # Образ дискеты: mdview2.exe + документация. Каждый документ — в СВОЕЙ @@ -46,10 +45,10 @@ $(DEMO_DISK): DEMO.MD | $(DISK_TMP) iconv -c -f UTF-8 -t CP1251 DEMO.MD > $@ || true floppy: $(EXAMPLE).exe $(DEMO_DISK) $(CHLOG_DISK) - python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe README.md $(DEMO_DISK) $(CHLOG_DISK) + $(PYTHON) $(MAKE_DISK) "$(FLOPPY_IMG)" $(EXAMPLE).exe README.md $(DEMO_DISK) $(CHLOG_DISK) @echo @echo "Floppy ready: $(FLOPPY_IMG)" - @echo "Run: cd $(MAME_DIR) && ./run_mame.sh" + @echo "Run: make run MAME_HOME=/путь/к/MAME/runtime" clean: rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP) diff --git a/examples/rpgwalk/Makefile b/examples/rpgwalk/Makefile index 0dccc2e..ecfe305 100644 --- a/examples/rpgwalk/Makefile +++ b/examples/rpgwalk/Makefile @@ -1,9 +1,8 @@ -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := rpgwalk EXTRA_FLAGS ?= --gfx 256 --memory tiny # rpgprof.exe в EXTRA_DATA — `make floppy` кладёт на дискету ОБА exe. EXTRA_DATA := bard1.atl bard2.atl bard.pal rpgprof.exe -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk # Профилировочная копия демо (бордер-полосы + маркеры для wpiset) — # собирается вместе с rpgwalk; инструкция в шапке rpgprof.c. diff --git a/examples/rpgwalk/conv_sprites.py b/examples/rpgwalk/conv_sprites.py index 680c181..97778a3 100644 --- a/examples/rpgwalk/conv_sprites.py +++ b/examples/rpgwalk/conv_sprites.py @@ -16,7 +16,7 @@ """ from PIL import Image -SRC = ("../../third_party/16x16-RPG-characters/sprites/" +SRC = ("../third_party/16x16-RPG-characters/sprites/" "old-style/02-bard.png") BASE = 16 # цвета PNG кладём после EGA-шестнадцати diff --git a/examples/scroll/Makefile b/examples/scroll/Makefile index 0aa7e3d..13c1bda 100644 --- a/examples/scroll/Makefile +++ b/examples/scroll/Makefile @@ -1,4 +1,3 @@ -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := scroll EXTRA_FLAGS ?= --gfx 256 -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk diff --git a/examples/sdk.mk b/examples/sdk.mk new file mode 100644 index 0000000..66c24be --- /dev/null +++ b/examples/sdk.mk @@ -0,0 +1,8 @@ +# Общая привязка примеров к установленному Sprinter-CC. +-include $(CURDIR)/../local.mk +SPRINTER_ROOT ?= $(if $(wildcard $(CURDIR)/../../bin/sprinter-cc),$(abspath $(CURDIR)/../..),) +ifeq ($(strip $(SPRINTER_ROOT)),) +$(error Задайте SPRINTER_ROOT или создайте Examples/local.mk) +endif +PROJ_ROOT := $(SPRINTER_ROOT) +include $(PROJ_ROOT)/app.mk diff --git a/examples/space/Makefile b/examples/space/Makefile index 7eb7526..35b13ef 100644 --- a/examples/space/Makefile +++ b/examples/space/Makefile @@ -1,8 +1,7 @@ -PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := space EXTRA_FLAGS ?= --gfx 256 --memory tiny EXTRA_DATA := space.atl -include $(PROJ_ROOT)/app.mk +include $(CURDIR)/../sdk.mk # Атлас: три ленты 16×16×4 (астероид/взрыв/маяк) → одна EMM-страница. space.atl: gen_sprites.py $(PROJ_ROOT)/toolchain/mkatlas.py diff --git a/examples/third_party/16x16-RPG-characters/LICENSE b/examples/third_party/16x16-RPG-characters/LICENSE new file mode 100644 index 0000000..604209a --- /dev/null +++ b/examples/third_party/16x16-RPG-characters/LICENSE @@ -0,0 +1,359 @@ +Creative Commons Legal Code + +Attribution-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined below) for the purposes of this + License. + c. "Creative Commons Compatible License" means a license that is listed + at https://creativecommons.org/compatiblelicenses that has been + approved by Creative Commons as being essentially equivalent to this + License, including, at a minimum, because that license: (i) contains + terms that have the same purpose, meaning and effect as the License + Elements of this License; and, (ii) explicitly permits the relicensing + of adaptations of works made available under that license under this + License or a Creative Commons jurisdiction license with the same + License Elements as this License. + d. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + e. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, ShareAlike. + f. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + g. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + h. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + i. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + j. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + k. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(c), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(c), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under the + terms of: (i) this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible + License. If you license the Adaptation under one of the licenses + mentioned in (iv), you must comply with the terms of that license. If + you license the Adaptation under the terms of any of the licenses + mentioned in (i), (ii) or (iii) (the "Applicable License"), you must + comply with the terms of the Applicable License generally and the + following provisions: (I) You must include a copy of, or the URI for, + the Applicable License with every copy of each Adaptation You + Distribute or Publicly Perform; (II) You may not offer or impose any + terms on the Adaptation that restrict the terms of the Applicable + License or the ability of the recipient of the Adaptation to exercise + the rights granted to that recipient under the terms of the Applicable + License; (III) You must keep intact all notices that refer to the + Applicable License and to the disclaimer of warranties with every copy + of the Work as included in the Adaptation You Distribute or Publicly + Perform; (IV) when You Distribute or Publicly Perform the Adaptation, + You may not impose any effective technological measures on the + Adaptation that restrict the ability of a recipient of the Adaptation + from You to exercise the rights granted to that recipient under the + terms of the Applicable License. This Section 4(b) applies to the + Adaptation as incorporated in a Collection, but this does not require + the Collection apart from the Adaptation itself to be made subject to + the terms of the Applicable License. + c. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Ssection 3(b), in the case of an + Adaptation, a credit identifying the use of the Work in the Adaptation + (e.g., "French translation of the Work by Original Author," or + "Screenplay based on original Work by Original Author"). The credit + required by this Section 4(c) may be implemented in any reasonable + manner; provided, however, that in the case of a Adaptation or + Collection, at a minimum such credit will appear, if a credit for all + contributing authors of the Adaptation or Collection appears, then as + part of these credits and in a manner at least as prominent as the + credits for the other contributing authors. For the avoidance of + doubt, You may only use the credit required by this Section for the + purpose of attribution in the manner set out above and, by exercising + Your rights under this License, You may not implicitly or explicitly + assert or imply any connection with, sponsorship or endorsement by the + Original Author, Licensor and/or Attribution Parties, as appropriate, + of You or Your use of the Work, without the separate, express prior + written permission of the Original Author, Licensor and/or Attribution + Parties. + d. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of the License. + + Creative Commons may be contacted at https://creativecommons.org/. diff --git a/examples/third_party/16x16-RPG-characters/README.md b/examples/third_party/16x16-RPG-characters/README.md new file mode 100644 index 0000000..b3b78a0 --- /dev/null +++ b/examples/third_party/16x16-RPG-characters/README.md @@ -0,0 +1,27 @@ +# 16x16 RPG characters `v3.0` + +16x16px RPG character sprite sheet for up to down games. + +This asset pack has been downloaded from https://route1rodent.itch.io/16x16-rpg-character-sprite-sheet + +Adapted from opengameart.org 's "[NES-Style RPG Characters](https://opengameart.org/content/nes-style-rpg-characters)" and "[More NES-style RPG Characters](https://opengameart.org/content/more-nes-style-rpg-characters)", with additional content made by [@route1rodent](https://route1rodent.itch.io). + +## License + +16x16 RPG character sprite sheet (c) by @route1rodent + +"16x16 RPG character sprite sheet" is licensed under a +Creative Commons Attribution-ShareAlike 3.0 Unported License (CC BY-SA 3.0). + +You should have received a copy of the license along with this +work. If not, see http://creativecommons.org/licenses/by-sa/3.0/. + + +## Credits + +Maintained by @route1rodent: + +- itch.io: https://route1rodent.itch.io +- Twitter: https://twitter.com/route1rodent +- Github: https://github.com/itsjavi +- Blog: https://blog.itsjavi.com diff --git a/examples/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png b/examples/third_party/16x16-RPG-characters/sprites/old-style/02-bard.png new file mode 100644 index 0000000000000000000000000000000000000000..98bf1c74f65525aaaca2783040619e148fe7ecca GIT binary patch literal 7830 zcmaKx2T)U6*Mxh0ivno+}xo7xIIK3I7tp0eWq&p!2cXqPLt_0DPqqI)M zT6P*QKyy(WgMhQs2aldp5+<0;;#U2`9J`t-3xBii+67I_gw>Z?1{@ z31QGVX`4Px5n_L9N(?taD}P5V<%S#k*~Pv=FW(1TOYC>>pHKcy!q{gIk#<}@HEFCN zh%1{b-FK+sG*!^D%No6JoUukvyvg4&MNTv%_@d2Ook27#gZ@*0VZ% zWBbocJMWUW8uB2a4{2VN%vJ7hjMqs;6cBoEWq?jy zTiJs7`GulCsx+BM?ZKxz|hA2n421l#jg0>eui&$OnpI zGo6;2-MtdAamc|D zYT5m6Su*#;pE=&3JS8E_LU0Yq2dgil+IU!6-`9#vcBMV$ED6x?RHVIW57q{X(9#g& z0fhbS&c{GQ&NAF{MQ9QCd}|m*)qY$pyGB4%J$%>y>QHMA#mWc-mv@-GTG25tI5!;cRL_PuqAh@aDudP(a)rnINtB2^X^PfJ~Cs=~KgH z4=1|z=^L_%3t<87ta%W(4$mqkCr`ZX#?~zp}HXf?&A8{#$H=n4jDa@g!kRA zj%z2Q<4Q!F_(WSnR!Nh3Ng7#Gv)|AYFhS8hs~Jobv%Bl8bbXUX(VU`T6qGjei%PzG zXWq_sg+woIkLq>9r8O_xjMgj6GYCf#F0nqnarHu81l;Cu?3q$%fW2s~Ye+~?#t9D@ z=$QEf`0@)V!v-hWdkU!s^P#N%+?SH+h5GT`fUYlxlzlX{tJ&H}Zn%AXCoHXHxfBHT zqWTl5Q>5axA>9waViB;>qqV-2lA%lR*2sPm@cvMm+;g$#oesW^RrLCR`-T?o$>{l( zEz$}M&}_3=w_W@=!c43AsfJ_%f0h)(dkNXbm9ai_6Pt9k8$aF9$*_Q>m}$ZK8R_;3 z^irDHl#{=RwbU$enQRxfUyXhWCmgC!@hv?k@rnsvLbkt1MRMcEqd5DRNW2njv$_Z> zJCm=w&CH{es7~h|w4ozx}vcG-j3j1i9;)%9b#v)h~gPfyyn#+?yv|@=R{6)M|vtXh4XhL z4mIS7Pd7}zU8nn8qzTY;*T2rQ%_$@y?l{VXIl$hoGpXZ=2tu$AKRt27i}8T`xS$Y; zk4inImVwUALWJ#Zc6*+Nal*uM+kIk%Fg9I8)bYfnZFgk$piduE8a)kaNj@6X(Nvrz z>~@D#7PPudi!M3`lYyNr+jvare2SdP*x`OG;}fqVPD|x!P5QXwH|Vmh9i+82JdQ*Z zMv+nEr9;z8!(|-uC`$(|5~{I)^-jOWE1W3@o<1>}<}(2K)u`|~8ALt;YHrea>xPzf z@uXU@HR`x6d$`T|)qU8M?Nj$2trJ{#u}JyKJ0K0yk}*YukHq0Hfi+!Lz`(#mDubrPs8%)sw9l&R3f$bj*;$S8Hcur=xi#jwDXcV zPFBtAmvZmX%RCDr*94={1*!KQG!`xtiJlYxYgwD@0nW~vYqqeic`~L*R6B_pc0VGI zb{NE%83TUygAa06AmO{A5z`?lbK%vZfCNjK>oV>(MSLyR+(SKsJVl`QS#BtZHFrd( zbcz^Xb?NUMj-?>?C44N{zkJ#^U3oV~-cv2$3E0v#1)t&v6=o#kv#+D9DfbR4Z{16( zMk|S^wIQT0_XhXoTBy*ZFq#R@H3Sn}%gZJ#bYf z_TPe|duBc;zfn$9J%Z-@k@u9j^wm3uI@osp1uq_JNd;tvA-HDc?^?YmTd%#(AOCOw z@?;%kO%7}P+*EhV6}2;9jFH>qp5TU3OjY(pMN*k^1}yi+azSOy;hdA))r6|gvthlp zH#QnqVrK$a3*{rMr3a0IXpco&R=o}IXS{{nw>u-<21x<#^vTaKH|o;DOv@}7io#GO zp_@O}V|Ph{^L29fqxq#i{4}E~>&kwvttiagSUth*qJ{e0Wg)MnZbGyU-$18NYuF}Y z+t6+}GrXK`3gd*RxMFQ-e2tR~3x>h=uxg4=(P4Ki5*3^P%oEK7y2sOPD5ef)FxDJH zZSpR7K9HWK3TTLjTDQI5lUq-oKElse(&0%xl;V;J@-a%*?k%y@m$n z0NU|6zaTMD3ETTzQ{Ow{>NZxzQ1vI2htf)FS<5gbVJhJ@bj+!9Imm;ElU4t8!|P%6 z^VX+M)ppB^b+#S$B^`NO(#^rTA(EtdaehSOx=l8KM;_mi!f~Uf9oxtw?{h)4vDPLL z1g?iFZym%)Ms)P5qn6l5c0XnzWB8pnkG(0>>2)df3pdmR?6`p2^27`JQBdP+#$Qqp zb2E-Ixsw%_CaiJ7%}Pl=MIcAmh7L3!zgm@VQ0ns>sg7^h(<<3amUv+gV1@t*VUYuf zJjK#_TDCLv1R}IiT)UJ){vX0E{+84z%Z2?Egz>H`)1_Bu%E#V5_nyFsQNhMNz$wNh z`xtFqMrg!Q_!)%LEqLEu?vq#q?%q+|#d1)^HvbBnwXlE-+(-r$zRUhhrJ|sH-0Y9} z`XAOCg+6FHpMtR0J_Qo3qRY}AfY0Gt;D^Kc8ea)ECm%Sj#p+VTxoDOxZmrTqSi>Uj z6j)p=JWtGv)8iZ1QKm@M*QoJ}r^}9hK_EN=mA~Aggvz|ahxQQoy~lyhCEAI+dCq2% zcaNY?L_7u|aE1~uO4pw)lgA~s`y&k(>yVN{o(FkWK^`7Ucp@)oz`%$3zz$yD|I5o^ zh8`>>9q5L(dCFb&+jjmvWD2VQvjTzT8!L9pOUlK%%2Z8Sbik@L%cFLFvNC=uXCNzn zrA?k#HCjh6?u5FmK;`W7>BW+!e6TtB zr*GwVw{F<}5u6J=f6GAskmtCf#nC?u-7cnUJ$d#-K|Hk7MD@r$%z*<>tDSoR$dmB4SOoRnji z?Zx~)$<+vJuf_lixBtdmp?JaGfrdbs%{tiWwXcw+^Rosc~&&$izFguFn;3LQgGQ`ziPz<+h8fD z&l?dzoeq! z)Y@t|SIz=*)1l2~#zDu+}FWXB#&6Zm+73EKEp|^PpXJO?UUN zks!mtZt&pBhs*#Py@t8w{0$VMX>-dNjuKb&owo_o1mN-;8)_z5bwnJt+LCn)@edty zfRz2kA1+W`!l-owaeI}DzcnRTU^HicBxOo{+dF`?rEIzuC6Iw$ZkfLD>daO^Z~A@j z+m2q0OyCYGV8fFckogxpI~r0QnhdHTHk>}^iU+3(T?m%zY%JbQW#BjeMd9+fe6|<} zInr}$RkE_%=#BvtxlQJeSv6C9060}`GAo; zLf_-AYPHs;eT_mhoC>E0)e4uvw26A={%Gb_IXrbQlXi7IJ)K3L%=#G-+Czp{D|{>H z&VP_EUQTM5s-tTt~AVJ=GLpEs{ETedW4lNnK%kd-(*A z7SL>L&_X2V46JaQ{iyv=Ee9NBXN*+G^2uA}a~D2oW$Ymyz!LHB@2dx1dX_$B!;EQh z7}MOc8q)SpHtV}4f#OsE+2T77WQab=a84>E=gn7#2Q<_Mf)$P?$`9Xv7k-?fxPkxl zaJ?oD6Q4ptXUpDVs&g|}_W*SF=Oe^Ud|?^L{Tn6%FJ`T?O)R3M8Q%21q*SnXG--k* zKG?*xA=Dm0IV_UqD+t6daXn!r1Ev@-~nQq76VPNm=clD|!Jp!b`7|s3wHVD`o$MscXNh z&bG8Z0UCA>U~Bxv)h;wooYix4iTwj#P&=yUy?7lv9?7x?j-mkd9Nl%ra4t8Pn#08# zO}!oU+hJW;m1h0e7zdL4@-JEifvbKyz3BKGInT!rp=73AlHc3Ej`5CDd!tk0jM_1% z7HwbgT|V$qt*SRUz`wxkSJf#b;fLR1_aT7+Mpu5qFxfxwR>sfaao6w0!M< zTPMp=P3+B=RlveO(NrSGhIH6E@J6E&IC~*b`0)FC1veX$BmM6aKA#UvCREb>*bR;E z-<>x8GpmNKYVQW>Nb;8BLqesXY)e1V+@a=9(UM??_B?1n^_aM?_~(}|(sw5Q6JEz$ zT9*iyL`9|@Bi^0PXrN9`o3!VNdQxo&-?dtG4d3~E<=1Xsz*w}Nzb-Ax^w9A7@@Vhk zJsm*~(Y<7x0}D!fRX#2jeN!xj_WB^Bbe>sgsL6BNRA$QE|N5dUN?)^p+36C!6k5SBr^5kP+G0)Wj#u_@4T&Am4x)KJuKT+t`I#C;BlttwZkSb zd7CJ^s2WmNS}yr3ANowtlx?iV@!&>P*yNlnYf)6ObWhUcuJwk`KS--}<_FTgW7T^h z>Gs!WnrF5?9F}@XV_rfF$8d*RaM`DKbaQHsq=`@6fU2LsrpOA znHUVBk>%;9QU?+}PNoEWKMKEjAS2Ukv(deKkCOfhQjlyEO{j(xUU3n8B9X;1*yt-) zIlNHdQRI@R7cB5H_FE8(TN#W;ALWcn{{+wjYOnY~R?w>C!TmNwt4VeC2|ZNef(x%e zZ^Gc2JIu|Lgk<1 zxWj^XnUjNmV{6>pDG$l+yRR`Gu`FYqdK}k{Lt=Xp1~(h7HIZqFtk|ycy??niheX0q z2D$7SKV9MNKy=E3IAK!_{#XeTZrl#s%YvVtoUEfdL^XwBg)GW4P3Ipbl{azUpM=D@ zd!vIdOSK_zfsVP@OUX-Ll$0Si6M(Eud3O+uH17LvWc7hju5L08x?3VO*Elb+C>9LH zUUJF*&1y!TWp3awTeZt-6_QzOol*PS4SM1lmXPl4#t+deTOv#4stPaB@)7=QbGD-L zOsh?dWW_bH3|=AZKSJrI4{jy9vhPdzQtQetp*MPxEPI&vN=V<;6$U?n5Re&4OGh*9 z7Z+`w`TeR%+n!f~8D48#>^gNe4dc)*-lZ1Bzl>yh1vOeoE;`-reQk6tRjd8a^7f-+0lR76fJOh|G zUcpl*<57*f8@tc`;_AtemAciRB&%IbFXeTVc(_h8F#cPb-1SQAcWRB!{0BjWjc?Jp zcN3#NzrFGQqAIf}Z}$s^G2Q2FNDZ@iVpHflsz>$TK$myW$3I-moniofaH>m^YvR+o zRqc}6S|bj0@F5RQMv8Mv5ivdKU1&t&&ys(e1Q`dq`m$J+bww}0--QBHxA19fN!ti) z3nol6LU@0_4-iJtFaH6fYyJA`Y!%Q>vyQ0R*ALiA_5h_cnU_BZmXDgZ2e4{%mU6x} zbF&68@hcC@)M-X^Mkee5o;HMUN}XJC0}U+UPA^R^I!TSQj~(C1qgjCDf^s>6DbwcX zdIq^Ndpjis%f93pumHI6{WWQ{a!{hYrxv7H><64KK+D7HjCH>t1Ld4J86&;vg9v&2 z$g(L+nALZhxW~KHtA-qELrd^}R##YFvhk;=?&qMOlGXF#v}wDjDm7Uet7!fOq+l#E z*zsR-%}_*maKdEmS4xaISjJ{zg;C{u)U`8=Jgx;;nacz2U?kR9TM;h%KLc#i%q|AN z6lC8YGty6qWuyk0mKmQrql9&5cXw+SOlo-uLm9XY~H?f_> z+|j2-9g(FKW(M&M-YN-dyr(?XgTR7fWik^mQTx;2rNOn|hrxS^~z2bvaJqoVM=HML%XMg?Mp$5PURc|31Ji?%PFTiy z3fWp!@RVPm<=S0Il*upsEdNAsa#izyRD)SOL zmX)Wp-72T$M*VE$oP&WQ%SJ63U70floe6&ftqfXcTF!r)CCgjjOk;U5>bZ@l!w4@E z^yl_uO#tze!bKA0XeFfGgcx+jWJb0t8eyv;6#TgTv^H)jYicxQ`miH?Gp=rywq37X zC)JQ(XruyesCKgFs-x4Oj+X{FtRAhgjtPoIRQks2rba#;KU9}9E76tC(@GA=tZeu= z_GAYABfVSIkd8X-K*htw0%b(~_hf6U%h+2tC4=L?>~UNy0=)>;m}2ew8Cj;H&*K+I zY!a|a8sCXHn;oJ*+J)s1mDTe!1V__*@lr9YF0y{RNxFCvA`34I&RYG#=uU~PK&#k{ zKF|j1T_fX4L2)cnOca{CM(-_#)Y1i-q3dj7G*gX*jz1iEs0lj&AjrOxn1Y%$(|dQ% z_*@$T>g_exWA`d0{g5;S_Y{~>Z+JE&f)U>82JJ% ztAq2Vj5r{ZmvEPG)0*e_U2Ds|USgeFIB)*n1WUYF{=6UjdVYSj%1W4cJ)ykBO^V7BcUK?YRI1~ z+s{}PsaS6Linrj$+dDB@Dh&;#A%^{tRi7+L%%aSMnVgA@eFFE$TTi0=Dc*HwRo+h* zY7+Zc9ZHsmJ+Mrm+r~cQ+C*V`9l{POj3lU@{8qg^V-~P@xTei+IUhB|$0@)aLFZ!_ zsI}^b3ik~t1s>%+=5$j{%&IsE^;>EtU3WD0_REun`QlOiYoAH1cEHCJhU48|6hqnm z#Z`TB{Ph2dvoO};>HJTOon=j@mUPGj29er%s{{%d^&?0YP9xYHQK1(U?n1hpisC6h6J-r!QU8ZPL=dmq!e zX73!?yySG(^)HOw`&#mBa-bEz_=kS`ithT8ZS~26Wnn80A%E%i@qqcV%kKKe)Nj8U zS%x)Poo?xVG{w^#vxGJzr*G2Q0z*ouyJ8kI;!_`hirKgnMwn@0loDGCX%Jib6JWuU zOQTM+Wc^QNo+@k z-UTr#Fzy~(h9|o~-VhM&RL@ONGj{QXa&?IJVT7XJ9^ZT`g@ S$U0~S7+g2jE&I(e>i+;CrcmSn literal 0 HcmV?d00001 diff --git a/tests/sdbg/run_mame_probe.py b/tests/sdbg/run_mame_probe.py index 8307f8a..5ee3625 100644 --- a/tests/sdbg/run_mame_probe.py +++ b/tests/sdbg/run_mame_probe.py @@ -4,7 +4,6 @@ Использует копию системного HDD и собственную дискету/каталоги состояния. Ничего не меняет в общих media или уже запущенном MAME. """ -import importlib.util import json import os from pathlib import Path @@ -26,6 +25,8 @@ from sdbg.model import DebugMap from sdbg.image import read_ihx from sdbg.session import DebugSession from mame_interactive import build_events +from make_disk import create_floppy_image +from mame_profile import MameProfile, write_keyboard_config def wait_dap_stop(engine, timeout=15): @@ -151,11 +152,13 @@ void worker(void) __banked { launcher.terminate() try: launcher.wait(timeout=8) except subprocess.TimeoutExpired: launcher.kill();launcher.wait() - spec=importlib.util.spec_from_file_location('make_disk',ROOT/'mame/v306/make_disk.py') - disk=importlib.util.module_from_spec(spec);spec.loader.exec_module(disk) - disk.create_floppy_image(str(work/'probe.img'),[('PROBE.EXE',str(exe))]) + profile=MameProfile.resolve() + if profile.binary is None and (ROOT/'mame/v306/mame.arm').is_file(): + profile=MameProfile.resolve({'MAME_HOME':str(ROOT/'mame/v306')}) + profile.validate(dss=True) + create_floppy_image(str(work/'probe.img'),[('PROBE.EXE',str(exe))]) system=work/'system.chd' - if not system.exists(): shutil.copyfile(ROOT/'mame/v306/IMG/sp_hdd_sys.chd',system) + if not system.exists(): shutil.copyfile(profile.system_hdd_image,system) events=build_events([(10,'a:\\probe.exe\n')]) ev=',\n'.join('{'+f'{t},"{tag}",{mask},{value}'+'}' for t,tag,mask,value in events) report=work/'result.jsonl' @@ -234,15 +237,17 @@ emu.register_periodic(function() end) '''.replace('REPORT',json.dumps(str(report))).replace('EVENTS',ev) .replace('SIGNATURE',','.join(map(str,signature))).replace('MAIN',str(main_address))) - command=[str(ROOT/'mame/v306/mame.arm'),'sprinter','-noreadconfig', - '-rompath',str(ROOT/'mame/v306/roms'),'-bios','v3.06','-kbd','ms_naturl,bios=sp2k', + command=[str(profile.binary),'sprinter','-noreadconfig', + '-rompath',str(profile.rompath),'-bios',profile.bios,'-kbd','ms_naturl,bios=sp2k', '-video','none','-sound','none','-nothrottle','-skip_gameinfo', '-beta:wd179x:0','35hd','-flop1',str(work/'probe.img'), + '-flop2',str(profile.dss_image), '-hard1',str(system),'-debug','-debugger','none', '-autoboot_delay','0','-autoboot_script',str(lua)] for key in ['nvram','cfg','diff','snapshot']: (work/key).mkdir(exist_ok=True) command.extend(['-'+key+'_directory',str(work/key)]) + write_keyboard_config(work/'cfg') bridge_mode = '--bridge' in sys.argv server_mode = '--server' in sys.argv env = dict(os.environ) @@ -254,8 +259,16 @@ end) ipc=work/('ipc-'+uuid.uuid4().hex) ipc.mkdir() env.update(SDBG_IPC_DIR=str(ipc),SDBG_SESSION_ID=uuid.uuid4().hex) + plugin_paths = [ROOT / 'toolchain/mcp'] + if profile.home: + installed = profile.home.parent / 'plugins' + if installed.is_dir(): + plugin_paths.append(installed) + legacy = ROOT / 'mame/sources/MAME/plugins' + if legacy.is_dir(): + plugin_paths.append(legacy) command.extend(['-verbose','-plugin','sdbgbridge','-pluginspath', - str(ROOT/'toolchain/mcp')+';'+str(ROOT/'mame/sources/MAME/plugins')]) + ';'.join(map(str, plugin_paths))]) with (work/'mame.log').open('w') as log: if not bridge_mode: result=subprocess.run(command,cwd=work,env=env,stdout=log,stderr=subprocess.STDOUT,timeout=55) diff --git a/tests/w3probe/Makefile b/tests/w3probe/Makefile index ebebd01..52c7f7b 100644 --- a/tests/w3probe/Makefile +++ b/tests/w3probe/Makefile @@ -14,7 +14,7 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..) CC := $(PROJ_ROOT)/bin/sprinter-cc MAKE_HDD := $(PROJ_ROOT)/toolchain/make_hdd.sh -HDD_IMG := $(PROJ_ROOT)/mame/v306/IMG/test_hdd.chd +HDD_IMG := $(CURDIR)/build/hdd/w3probe.chd EXES := w3probe.exe w3tiny.exe w3big.exe w3huge.exe @@ -34,7 +34,8 @@ w3huge.exe: w3huge.c w3res.c w3huge_res.c hugebank.c # Упаковать все четыре на HDD (D:) для прогона в MAME. После — рестарт MAME. hdd: $(EXES) - $(MAKE_HDD) $(HDD_IMG) $(EXES) + mkdir -p $(dir $(HDD_IMG)) + CHDMAN_BIN="$(CHDMAN_BIN)" $(MAKE_HDD) $(HDD_IMG) $(EXES) clean: rm -rf .sprinter-cc-* $(EXES) diff --git a/toolchain/make_disk.py b/toolchain/make_disk.py new file mode 100755 index 0000000..fcb7fcf --- /dev/null +++ b/toolchain/make_disk.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Упаковщик FAT12-дискеты Sprinter. Образ принадлежит вызывающему проекту.""" + +import struct +import sys +import os + +def create_floppy_image(output_file, files_to_add): + """Создаёт FAT12 образ дискеты 1.44MB и добавляет файлы""" + + # Размер дискеты 1.44MB + SECTOR_SIZE = 512 + TOTAL_SECTORS = 2880 + FAT_SIZE = 9 + ROOT_DIR_ENTRIES = 224 + + # Проверяем весь набор до открытия выходного файла: ошибочная упаковка + # не должна уничтожать предыдущий пригодный носитель приложения. + capacity = TOTAL_SECTORS - 1 - 2 * FAT_SIZE - ROOT_DIR_ENTRIES * 32 // SECTOR_SIZE + needed = 0 + names = set() + if len(files_to_add) > ROOT_DIR_ENTRIES: + raise ValueError("слишком много файлов для корневого каталога FAT12") + for filename, src_path in files_to_add: + name, ext = os.path.splitext(filename) + short_name = (name[:8].upper(), ext[1:4].upper()) + # Исторические тесты SDK могут иметь длинное имя; на носителе оно + # сокращается до 8.3. Коллизия после сокращения — настоящая ошибка. + if not name or short_name in names: + raise ValueError(f"пустое или повторное имя FAT 8.3: {filename}") + short_name[0].encode('ascii') + short_name[1].encode('ascii') + names.add(short_name) + needed += max(1, (os.path.getsize(src_path) + SECTOR_SIZE - 1) // SECTOR_SIZE) + if needed > capacity: + raise ValueError(f"данные не помещаются на дискету: {needed} > {capacity} секторов") + + os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True) + + # Создаём пустой образ + with open(output_file, 'wb') as f: + f.write(b'\x00' * (TOTAL_SECTORS * SECTOR_SIZE)) + + # Читаем и модифицируем образ + with open(output_file, 'r+b') as f: + # BPB (BIOS Parameter Block) - начинаем с смещения 0 + # Jump instruction + f.seek(0) + f.write(b'\xEB\x3C\x90') + # OEM name + f.write(b'MSDOS5.0') + # Bytes per sector + f.write(struct.pack('