Подготовить автономную сборку и запуск перед разделением проектов
This commit is contained in:
@@ -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/
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -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.
|
||||
@@ -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,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
|
||||
|
||||
@@ -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 персонажей листа берём
|
||||
|
||||
|
||||
@@ -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 гоняет НАСТОЯЩУЮ физику, а она ходит по таблицам анимации
|
||||
|
||||
@@ -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
|
||||
BIN
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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user