- Implemented a local web server using http.server to serve the save editor. - Created a user interface with HTML, CSS, and JavaScript for managing game slots and editing save files. - Added API endpoints for retrieving slot information, overview, and raw data for editing. - Implemented functionality for saving changes back to the game files. - Added tests for ARR and DTA formats to ensure data integrity during round-trip serialization. Co-authored-by: Codex <noreply@openai.com>
268 lines
12 KiB
Markdown
268 lines
12 KiB
Markdown
# Reksio i Czarodzieje — model zapisu gry
|
||
|
||
Rozpoznanie na podstawie zdekodowanych skryptów (`skrypty_zdekodowane/ric`) i plików gry
|
||
(`Reksio i Czarodzieje/`). Wersja gry: `Czarodzieje.exe` z 2004-05-19, PIKLIB8.dll.
|
||
|
||
## 1. Gdzie leży stan gry
|
||
|
||
Wszystko siedzi w jednym katalogu: `<gra>/common/` (w skryptach: `$COMMON\`).
|
||
Nie ma żadnej dedykowanej klasy `SAVEGAME` ani jednego pliku zapisu — stan to
|
||
**~76 osobnych plików na slot**, kopiowanych plik-po-pliku przez `COPYFILE`.
|
||
|
||
Sloty są zakodowane **cyfrą doklejoną do nazwy pliku**:
|
||
|
||
| sufiks | znaczenie |
|
||
|---|---|
|
||
| `_DEF` | fabryczny stan początkowy (nie ruszać — to szablon dla „Nowa gra") |
|
||
| `0` | **żywy stan bieżącej rozgrywki** — to czyta i zapisuje gra w trakcie grania |
|
||
| `1`–`4` | cztery sloty zapisu z menu |
|
||
|
||
Kluczowy wniosek dla edytora: **slot 0 to nie jest „pierwszy zapis", tylko working set.**
|
||
Wejście do gry po edycji slotu 1 wymaga wczytania go z menu (albo skopiowania `1 → 0`
|
||
samemu, co robi rutyna `LOADGAME`).
|
||
|
||
## 2. Dwa formaty plików
|
||
|
||
### 2.1 `.ARR` / `.SAV` — obiekt `ARRAY` (binarny)
|
||
|
||
Little-endian, bez nagłówka i bez wersji:
|
||
|
||
```
|
||
uint32 count
|
||
count × {
|
||
uint32 type // 1 = INTEGER, 2 = STRING
|
||
type==1: int32 value
|
||
type==2: uint32 len; byte[len] value // bez terminatora, cp1250
|
||
}
|
||
```
|
||
|
||
Zweryfikowane na wszystkich 66 plikach `.arr` w `common/` — round-trip bajt w bajt,
|
||
występują wyłącznie typy 1 i 2. Pliki `*.SAV` w katalogach scen (`Dragon/DRAGON_0.SAV`,
|
||
`Labirynt/LABIRYNT_0.SAV`) to **ten sam format** — to zwykłe `ARRAY^SAVE()`.
|
||
|
||
Uwaga: liczby bywają zapisane jako **string** (typ 2), bo silnik konwertuje w locie —
|
||
np. `GAME0.ARR` trzyma `G_ITKTPM` jako `"6"`. Nie zakładaj typu po znaczeniu pola.
|
||
|
||
### 2.2 `.DTA` — obiekt `DATABASE` (tekstowy)
|
||
|
||
Wiersze rozdzielone `\r\n`, kolumny `|`, brak nagłówka, brak cudzysłowów, pusta wartość
|
||
to literalne `NULL`. Kodowanie cp1250 (w praktyce w zapisach jest samo ASCII).
|
||
|
||
Drobiazg do round-tripu: pliki `*_def.dta` (autorskie) **nie mają** końcowego `\r\n`,
|
||
a te zapisane przez grę (`*0.DTA`) **mają**. Parser musi tolerować oba, writer domyślnie
|
||
emituje wariant „gry".
|
||
|
||
Schemat kolumn nie jest w pliku — siedzi w definicji `STRUCT` w `.cnv`
|
||
(`Arcade.cnv:1455-1489`):
|
||
|
||
```
|
||
SOBJECT (sceny): NAME<STRING> | IDNAME<STRING> | TYPE<INTEGER>
|
||
| SPARAM0<STRING> | SPARAM1<STRING> | SPARAM2<STRING>
|
||
| IPARAM0<INTEGER> | IPARAM1<INTEGER> | IPARAM2<INTEGER>
|
||
|
||
SITEM (ekwipunek): GUID<INTEGER> | NAME<STRING> | PARENT<INTEGER>
|
||
| BASE<STRING> | EMPTY<INTEGER>
|
||
```
|
||
|
||
## 3. Manifest slotu
|
||
|
||
Źródło: `MAINMENU.class` → `SAVEGAME` (linia 667) i `LOADGAME` (linia 672).
|
||
|
||
| plik | typ | liczba | co trzyma |
|
||
|---|---|---|---|
|
||
| `<SCENA><s>.DTA` | DTA/SOBJECT | 52 | stan obiektów każdej lokacji |
|
||
| `ITEMS<s>.DTA` | DTA/SITEM | 1 | ekwipunek (10 stałych slotów) |
|
||
| `GAME<s>.ARR` | ARR | 1 | globalne zmienne rozgrywki (6 pól) |
|
||
| `SPELLS<s>.ARR` | ARR | 1 | 17 intów — znane czary |
|
||
| `CUTSCENES<s>.ARR` | ARR | 1 | obejrzane przerywniki |
|
||
| `INVEST<s>.ARR` | ARR | 1 | stan karty śledztwa (`INVESTIGATION.class`) |
|
||
| `QUESTIONS<s>.ARR` | ARR | 1 | pytania, które Reksio *może* zadać |
|
||
| `QUESTIONS_ASKED<s>.ARR` | ARR | 1 | pytania już zadane (globalnie) |
|
||
| `<POSTAĆ>_HIST<s>.ARR` | ARR | 11 | historia rozmowy z daną postacią |
|
||
| `<MINIGRA><s>.ARR` | ARR | 6 | postęp minigier |
|
||
| `m_shot<s>.img` | IMG | 1 | miniaturka zapisu (tylko sloty 1–4) |
|
||
|
||
**52 lokacje** (kolejność z `G_ARRDATAS`, `Game.cnv:BFITMP125`):
|
||
`PODWIECZOREK1/2/3, TRZYWEJSCIA, DOMALCHOMIKA, GABINETDYR, HOLGLOWNY, UNIVFRONT,
|
||
BIALO, MIASTO, LUSTRO, TELEP_PODWORKO, TELEP_WARSZTAT, TELEP_KURNIKI, PIWNICA,
|
||
ALCHEMY, RATUSZ, LABIRYNTH, PIKLIBIA, DOMSPIEL, PRZEPASC, MIOTLISKO, KAPTUREK,
|
||
CALINECZKA, DRATEWKA, SNIEZKA, KROLEWNA, JAGA, OLBRZYM, SZREKSIO, TROL, NORASZ,
|
||
SEZAM, PRZEDSEZAMEM, KROLOWA, SMOK, RATUSZIN, GULDRYK, KORYTARZ, SALA, UNIVBACK,
|
||
UNIVPIWNICA, MIOTSHOP, PRZYSTAN, GABINETSNEJKA, BARANMIOT, WMWEJSCIE, WMWNETRZE,
|
||
PIKMIOT, SKLEPALCH, ARRAS, POMPA`
|
||
|
||
**11 postaci dialogowych** (`M_ARRCHARACTERS`):
|
||
`BUREKTOR, GULDRYK, SNEJK, WALDIMORS, SPIELMAUSTER, CHRUMBURAK, BARANDALF,
|
||
KROLOWA, KAMIEN, SMOK, GASIENICA`
|
||
|
||
**6 minigier** (`M_ARRPIOTR`): `LABIRYNT, MIOTLY, CAT, BARANDALF, DRAGON, SHOOTER`
|
||
|
||
### Czego `SAVEGAME` **nie** zapisuje
|
||
|
||
- `SETTINGS.ARR` — globalne, wspólne dla wszystkich slotów: `[?, głośność 0..800]`
|
||
- `Dane/Game/Przygoda/*/*_0.SAV` — osobne pliki postępu minigier w katalogach scen
|
||
- `SLOT_<prefix>_<n>.SAV`, `DODOS_<prefix>.SAV` — stan alchemii (`Alchemy.cnv`),
|
||
zapisywane bezpośrednio do `$COMMON`, **poza** manifestem slotu → przeżywają
|
||
wczytanie innego zapisu
|
||
|
||
## 4. `GAME<s>.ARR` — 6 pól i cała reszta
|
||
|
||
To jest najważniejszy plik: mówi gdzie stoisz. Wszystkie 6 elementów to **stringi**.
|
||
|
||
| idx | zmienna globalna | znaczenie |
|
||
|---|---|---|
|
||
| 0 | `G_SARCADEOBJECTS` | bieżąca lokacja (nazwa z listy 52) |
|
||
| 1 | `G_SLASTOBJECTS` | poprzednia lokacja (skąd przyszedłeś → punkt wejścia) |
|
||
| 2 | `G_SDIALOGCHARACTER` | z kim rozmowa |
|
||
| 3 | `G_SDIALOGRETURN` | dokąd wrócić po rozmowie |
|
||
| 4 | `G_ITKTPM` | losowe 0..7, ustalane raz na nową grę (`DEFSETS`) |
|
||
| 5 | `G_IKRETACTIVE` | czy Kret towarzyszy Reksiowi (0/1) |
|
||
|
||
Domyślne dla nowej gry (`DEFSETS`): `["INTRO_1", "NULL", "BUREKTOR", "NULL", rand(0,7), "0"]`.
|
||
Jeśli plik ma <6 elementów, gra po cichu resetuje do domyślnych — czyli **zły rozmiar
|
||
= utrata pozycji**, a nie crash.
|
||
|
||
## 5. `<SCENA><s>.DTA` — kolumna `TYPE` (dekoder z `Arcade.cnv:__LOAD_DB__`)
|
||
|
||
| TYPE | rola wiersza |
|
||
|---|---|
|
||
| `< 100` | zwykły obiekt sceny → ładowany jako `ANNOBJECT<n>` z pliku `NAME` |
|
||
| `101` | tło statyczne (`CANVASOBSERVER^SETBACKGROUND`), `NAME` = `.IMG` |
|
||
| `102` | maska ścieżek chodzenia (`WPATH^LOAD`), `NAME` = `.SEK` |
|
||
| `103` | aktywacja przejścia: `WPATH^SETACTIVE(IPARAM0, IPARAM1, IPARAM2)` |
|
||
| `104` | Reksio; `NAME` = `.ANN`, `IPARAM0` = prędkość maks. |
|
||
| `105` / `145` | punkt startowy (zestaw 0 / 1); `IDNAME` = scena skąd, `IPARAM0/1` = X/Y |
|
||
| `106` / `146` | punkt docelowy (zestaw 0 / 1); jw. |
|
||
| `107` | tło animowane (`ANNBKG`), `IPARAM0/1` = szerokość/wysokość |
|
||
| `108` | muzyka lokacji, `IDNAME` = nazwa `.WAV` |
|
||
| `140` | Kret (`ANNKRET`), `NAME` = `.ANN` |
|
||
|
||
Dla obiektów z `TYPE == 2` `IPARAM0` jest **flagą stanu**: `1` → obiekt widoczny,
|
||
`0` → schowany/zabrany (`BFITMP1045` → `1043`/`1044`). Pozostałe `TYPE < 100` są zawsze
|
||
pokazywane, więc tam `IPARAM0/1/2` znaczą co innego per scena — to trzeba dekodować
|
||
scena po scenie z odpowiedniego `.cnv` (np. `Piklibia.cnv:BEHSAVEMAP` używa `IPARAM0`
|
||
jako numeru klatki, a `IPARAM1` jako stanu rycerza).
|
||
|
||
## 6. Ekwipunek — `ITEMS<s>.DTA`
|
||
|
||
Zawsze dokładnie **10 wierszy**, indeksowanych `GUID` 0..9. Pusty slot to
|
||
`n|NULL|0|NULL|0`. Pola:
|
||
|
||
- `GUID` — indeks slotu, równy numerowi wiersza
|
||
- `NAME` — identyfikator przedmiotu, np. `BUTY_BUT`; grafika ładowana po nazwie
|
||
- `PARENT` — indeks wiersza w `<SCENA>.DTA`, z którego przedmiot został podniesiony
|
||
(`-1` = znikąd). Przy odkładaniu gra ustawia temu obiektowi `IPARAM0 = 1`
|
||
- `BASE` — lokacja pochodzenia (wartość `G_SARCADEOBJECTS` w chwili podniesienia)
|
||
- `EMPTY` — mylące: **`1` = slot zajęty**, `0` = wolny (`__LOAD_ITEMS__` pokazuje
|
||
przedmiot gdy `EMPTY == 1`)
|
||
|
||
## 7. Dialogi
|
||
|
||
Trzy warstwy, dwie statyczne i jedna w zapisie:
|
||
|
||
- `common/dialogs.dta` (statyczne, 93 wiersze) — `ID | plik_pytania.WAV | plik_odpowiedzi.WAV | trigger`.
|
||
Ostatnia kolumna to ID pytania odblokowywanego po zadaniu tego.
|
||
- `common/<POSTAĆ>.arr` (statyczne, małe litery) — pełna pula pytań danej postaci,
|
||
np. `GULDRYK.arr = ["50","51","52","53"]`
|
||
- `common/<POSTAĆ>_HIST<s>.ARR` (**w zapisie**) — pytania już zadane tej postaci
|
||
- `common/QUESTIONS<s>.ARR` (**w zapisie**) — pytania, które Reksio aktualnie *ma*
|
||
(`ARRBASEQUESTIONS`); zadanie pytania usuwa je stąd i dokłada do `_HIST`
|
||
- `common/QUESTIONS_ASKED<s>.ARR` (**w zapisie**) — globalny log zadanych pytań
|
||
|
||
ID pytań to stringi, nie liczby (`"11A"`, `"11B"`) — parser nie może ich rzutować na int.
|
||
|
||
## 8. Czary i doświadczenie — `SPELLS<s>.ARR`
|
||
|
||
17 intów, jedna tablica na czary **i** na klepsydrę. Stan fabryczny (`NEWGAME`):
|
||
same zera poza indeksem 12 = 64.
|
||
|
||
| idx | znaczenie |
|
||
|---|---|
|
||
| 0–11 | flagi „czar znany" (0/1) |
|
||
| 12 | start 64, `SUBAT(12,3)` przy każdym awansie — **nikt tego nie czyta** |
|
||
| 13 | **doświadczenie**, 0..149 |
|
||
| 14 | licznik awansów |
|
||
| 15 | 1 → klepsydra/różdżka widoczna w menu |
|
||
| 16 | 1 → klepsydra miga, można nauczyć się czaru |
|
||
|
||
Czary to sześć kul w dwóch wariantach — zwykłym (0–5) i obronnym (6–11, sufiks
|
||
`_OBR`); czar `N` ma obronę `N+6`:
|
||
|
||
| idx | czar | idx | obrona | gabinet |
|
||
|---|---|---|---|---|
|
||
| 0 | `KULA_SEN` | 6 | `KULA_SEN_OBR` | Guldryk |
|
||
| 1 | `KULA_ZABA` | 7 | `KULA_ZABA_OBR` | Snejk / Wmwnetrze |
|
||
| 2 | `KULA_MUCHY` | 8 | `KULA_MUCHY_OBR` | Snejk |
|
||
| 3 | `KULA_BUDYN` | 9 | `KULA_BUDYN_OBR` | Guldryk |
|
||
| 4 | `KULA_WIRY` | 10 | `KULA_WIRY_OBR` | Wmwnetrze |
|
||
| 5 | `KULA_CIEMNOSC` | 11 | `KULA_CIEMNOSC_OBR` | Wmwnetrze / Snejk |
|
||
|
||
Scena `MAGIC` czyta indeksy 0..11 do `ARRACTIVESPELLS` (`Magic.cnv:1329`), a nauka
|
||
czaru to `CHANGEAT(<idx>, 1)` + natychmiastowy `SAVE("$COMMON\SPELLS0.ARR")`
|
||
(`Magic.cnv:BFITMP49`, `Guldryk.cnv:BFITMP763`).
|
||
|
||
### Mechanika klepsydry (`MAINMENU.class`)
|
||
|
||
```
|
||
ADDEXP(n) :737 G_ARRREXSPELLS^ADDAT(13, n) → odśwież klepsydrę → SAVEEXP
|
||
SAVEEXP :712 SAVE("$COMMON\SPELLS0.ARR") ← zapis NATYCHMIAST, przy każdym punkcie
|
||
BFITMP57 :1022 exp < 150 ? rysuj klatkę : miganie
|
||
BFITMP55 :1012 klatka klepsydry = exp / 10 + 1 (16 klatek na 150 exp)
|
||
BFITMP56 :1017 PLAY("MIGA") + CHANGEAT(16, 1)
|
||
LEVELUP :743 exp w [0,150) → FALSE; inaczej BFITMP61
|
||
BFITMP61 :1042 exp -= 150 (reszta przechodzi dalej); idx14 += 1; idx12 -= 3;
|
||
PLAY("PANEL"); CHANGEAT(16, 0)
|
||
```
|
||
|
||
Punkty przyznaje `G_MENU^ADDEXP(n)` rozsiane po scenach — ok. 60 wywołań, wartości
|
||
10/20/30/40/50. Rozwiązanie zagadki daje 50 (`Arcade.cnv:_SOLVED_`), rozmowa 30
|
||
(`Dialogs.cnv:BFITMP51`).
|
||
|
||
Ważne dla edytora: doświadczenie **jest zapisywane od razu do slotu 0**, przy każdym
|
||
punkcie, niezależnie od menu „Zapisz". Nie trzeba go szukać nigdzie indziej.
|
||
|
||
### Dwa błędy w oryginale
|
||
|
||
1. `BFITMP61` zapisuje przez `G_ARRREXSPELLS^SAVE("SPELLS0.DTA")` — **zła nazwa
|
||
i złe rozszerzenie**, bez `$COMMON\`. Sam awans nie trafia do `SPELLS0.ARR` tą
|
||
drogą; ratuje to dopiero najbliższy `ADDEXP` → `SAVEEXP`.
|
||
2. `SAVEEXP` sprawdza `GETSIZE() < 16`, a w ścieżce awaryjnej (`BFITMP49`) tworzy
|
||
tablicę **16-elementową**, podczas gdy reszta kodu oczekuje 17 i sięga po
|
||
indeks 16. Przy normalnej grze nie wypływa, bo `NEWGAME` tworzy 17.
|
||
|
||
## 9. Przepływ Nowa gra / Zapis / Wczytanie
|
||
|
||
```
|
||
NEWGAME (MAINMENU.class:657)
|
||
<SCENA>_DEF.DTA → <SCENA>0.DTA (×52)
|
||
ITEMS_DEF.DTA → ITEMS0.DTA
|
||
QUESTIONS_DEF.ARR → QUESTIONS0.ARR, QUESTIONS_ASKED0.ARR,
|
||
<POSTAĆ>_HIST0.ARR (×11), INVEST0.ARR
|
||
<MINIGRA>_DEF.ARR → <MINIGRA>0.ARR (×6)
|
||
CUTSCENES0.ARR := []
|
||
SPELLS0.ARR := [0×12, 64, 0×4]
|
||
GAME0.ARR := DEFSETS
|
||
|
||
SAVEGAME(n) (linia 667) LOADGAME(n) (linia 672)
|
||
0 → n dla wszystkich plików n → 0 dla wszystkich plików
|
||
+ GAME0 budowane z żywych + GAME<n> wczytane DO żywych globali
|
||
globali, potem zapisane (jeśli <6 elementów → DEFSETS)
|
||
+ SAVE.IMG → m_shot<n>.img
|
||
```
|
||
|
||
Scena wchodzi w stan po wczytaniu przez procedurę `_LOADGAME_` wystawioną w każdym
|
||
`.cnv` sceny (`Arcade.cnv:1533`) — ta tylko robi `GOTO` do `G_SARCADEOBJECTS`,
|
||
cała reszta stanu przyjeżdża z plików przy `__INIT__` → `__LOAD_DB__`.
|
||
|
||
## 10. Ryzyka przy edycji
|
||
|
||
- **Liczba wierszy `.DTA` musi się zgadzać** — kod scen adresuje wiersze po indeksie
|
||
(`DBOBJECTS^SELECT(10)`, `BEHSELECTOBJ^RUN(23)`). Dodanie/usunięcie wiersza przesunie
|
||
wszystko i rozjedzie logikę. Edytuj wartości, nie strukturę.
|
||
- **`ITEMS` zawsze 10 wierszy** — pętle są zahardkodowane na `0..10`.
|
||
- **Za krótkie `GAME`/`SPELLS`** → cichy reset do domyślnych, nie błąd.
|
||
- Gra trzyma pliki otwarte tylko na czas operacji, ale i tak edytuj przy zamkniętej grze —
|
||
`SAVEGAME` nadpisze slot bez pytania.
|
||
- Alchemia (`SLOT_*.SAV`, `DODOS_*.SAV`) jest poza slotami — po wczytaniu starego zapisu
|
||
zostaje stan z najnowszej rozgrywki. To bug w grze, nie w edytorze.
|