Files

12 KiB
Raw Permalink Blame History

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
14 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.classSAVEGAME (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 14)

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 identyfikator "Tego, Który Tak Potwornie Mąci", 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 (BFITMP10451043/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
011 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 (05) i obronnym (611, 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 ADDEXPSAVEEXP.
  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.