# 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: `/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 | IDNAME | TYPE | SPARAM0 | SPARAM1 | SPARAM2 | IPARAM0 | IPARAM1 | IPARAM2 SITEM (ekwipunek): GUID | NAME | PARENT | BASE | EMPTY ``` ## 3. Manifest slotu Źródło: `MAINMENU.class` → `SAVEGAME` (linia 667) i `LOADGAME` (linia 672). | plik | typ | liczba | co trzyma | |---|---|---|---| | `.DTA` | DTA/SOBJECT | 52 | stan obiektów każdej lokacji | | `ITEMS.DTA` | DTA/SITEM | 1 | ekwipunek (10 stałych slotów) | | `GAME.ARR` | ARR | 1 | globalne zmienne rozgrywki (6 pól) | | `SPELLS.ARR` | ARR | 1 | 17 intów — znane czary | | `CUTSCENES.ARR` | ARR | 1 | obejrzane przerywniki | | `INVEST.ARR` | ARR | 1 | stan karty śledztwa (`INVESTIGATION.class`) | | `QUESTIONS.ARR` | ARR | 1 | pytania, które Reksio *może* zadać | | `QUESTIONS_ASKED.ARR` | ARR | 1 | pytania już zadane (globalnie) | | `_HIST.ARR` | ARR | 11 | historia rozmowy z daną postacią | | `.ARR` | ARR | 6 | postęp minigier | | `m_shot.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__.SAV`, `DODOS_.SAV` — stan alchemii (`Alchemy.cnv`), zapisywane bezpośrednio do `$COMMON`, **poza** manifestem slotu → przeżywają wczytanie innego zapisu ## 4. `GAME.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. `.DTA` — kolumna `TYPE` (dekoder z `Arcade.cnv:__LOAD_DB__`) | TYPE | rola wiersza | |---|---| | `< 100` | zwykły obiekt sceny → ładowany jako `ANNOBJECT` 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.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 `.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/.arr` (statyczne, małe litery) — pełna pula pytań danej postaci, np. `GULDRYK.arr = ["50","51","52","53"]` - `common/_HIST.ARR` (**w zapisie**) — pytania już zadane tej postaci - `common/QUESTIONS.ARR` (**w zapisie**) — pytania, które Reksio aktualnie *ma* (`ARRBASEQUESTIONS`); zadanie pytania usuwa je stąd i dokłada do `_HIST` - `common/QUESTIONS_ASKED.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.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(, 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) _DEF.DTA → 0.DTA (×52) ITEMS_DEF.DTA → ITEMS0.DTA QUESTIONS_DEF.ARR → QUESTIONS0.ARR, QUESTIONS_ASKED0.ARR, _HIST0.ARR (×11), INVEST0.ARR _DEF.ARR → 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 wczytane DO żywych globali globali, potem zapisane (jeśli <6 elementów → DEFSETS) + SAVE.IMG → m_shot.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.