Подготовить автономную сборку и запуск перед разделением проектов

This commit is contained in:
Александр Петров
2026-09-15 23:04:04 +03:00
parent 5323b168a7
commit 0e74aaa7ee
116 changed files with 6908 additions and 333 deletions
+16 -38
View File
@@ -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
+14 -9
View File
@@ -27,9 +27,9 @@ banked-call trampolines, graphics & accelerator API, mouse driver wrappers, and
git clone <this repo> 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`)
+44 -26
View File
@@ -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
+29
View File
@@ -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/
+1
View File
@@ -0,0 +1 @@
3.12
+21
View File
@@ -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.
+4 -5
View File
@@ -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)
+1 -2
View File
@@ -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
+2 -3
View File
@@ -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
@@ -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"
@@ -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"
@@ -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 персонажей листа берём
+8 -9
View File
@@ -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 $@; }
@@ -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 гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
+11
View File
@@ -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
@@ -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/.
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -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 <stdint.h>
#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()
@@ -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()
+1 -1
View File
@@ -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
@@ -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()
+4 -3
View File
@@ -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
+4
View File
@@ -62,6 +62,10 @@ build/
# ---------------------------------------------------------------------------
# ОС / редакторы / локальные настройки
# ---------------------------------------------------------------------------
local.mk
.resource-stamps/
__pycache__/
*.py[cod]
.DS_Store
._*
*~
+21
View File
@@ -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.
+10 -13
View File
@@ -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
+3 -2
View File
@@ -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` |
+5 -1
View File
@@ -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
+15
View File
@@ -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/
+1
View File
@@ -0,0 +1 @@
3.12
+21
View File
@@ -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.
+2 -3
View File
@@ -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)
+11
View File
@@ -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
+41
View File
@@ -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.
+8
View File
@@ -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` и эталонных кадров.
+153
View File
@@ -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 в корне репозитория).
File diff suppressed because it is too large Load Diff
+304
View File
@@ -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 <stdint.h>
#include <palette.h> /* 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 */
@@ -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 */
+319
View File
@@ -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 <stdint.h>
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в attr_is_content */
#include <sprinter.h> /* sprinter_page_w3 */
#include <sprinter_mem.h> /* 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<file_size) это просто
* обрезка, не считаем за ошибку. */
if (cont && n == file_size) utf_ok = 0;
if (!has_high) return ENC_CP866; /* чистый ASCII */
if (utf_ok && has_mb) return ENC_UTF8;
if (s1251 >= 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;
}
@@ -0,0 +1,106 @@
/*
* mdview2_help.c — диалог справки (F1).
*
* Модальное окно поверх документа: рамка с заголовком, список горячих
* клавиш и поддерживаемых markdown-элементов. Блокирует до нажатия любой
* клавиши, после чего восстанавливает статус-бар, область документа и меню.
*
* Зависимости от ядра (mdview2.c): render_full_status / draw_viewport_from_cache
* / render_menu для восстановления экрана. Текст справки — в CP866.
*/
#include <stdint.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#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();
}
+209
View File
@@ -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 <stdint.h>
#include <string.h>
#include <conio.h>
#include <bios/text.h>
#include <sprinter_mem.h> /* 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 */
File diff suppressed because it is too large Load Diff
+227
View File
@@ -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 <stdint.h>
#include <conio.h> /* wrchar/scroll/COLOR */
#include <bios/text.h> /* 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;
}
+308
View File
@@ -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 <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <bios/text.h>
#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 */
@@ -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 <stdint.h>
#include <stdio.h> /* dec8/dec16, cputs */
#include <conio.h> /* textattr/gotoxy/wrchar/COLOR */
#include <limits.h>
#include <bios/text.h> /* 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);
}
@@ -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 <stdint.h>
#include <conio.h> /* 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);
}
}
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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
@@ -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"
@@ -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"
@@ -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()
{
@@ -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" \
@@ -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)"
@@ -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)"
@@ -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"
@@ -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"
+2 -1
View File
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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"
@@ -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
@@ -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
+1 -2
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+12 -29
View File
@@ -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
+6 -4
View File
@@ -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 держать рядом с примером либо получать по зафиксированному рецепту. |
+22
View File
@@ -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/
+1
View File
@@ -0,0 +1 @@
3.12
+21
View File
@@ -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.
+11
View File
@@ -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
+22
View File
@@ -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`.
+1 -2
View File
@@ -1,4 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := balls
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
include $(CURDIR)/../sdk.mk
+3 -4
View File
@@ -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)
+3 -4
View File
@@ -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)
+1 -2
View File
@@ -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.
+1 -1
View File
@@ -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-шестнадцати
+1 -2
View File
@@ -1,4 +1,3 @@
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := scroll
EXTRA_FLAGS ?= --gfx 256
include $(PROJ_ROOT)/app.mk
include $(CURDIR)/../sdk.mk
+8
View File
@@ -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
+1 -2
View File
@@ -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
+359
View File
@@ -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/.
+27
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More