12 KiB
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 scenSLOT_<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 (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 wierszaNAME— identyfikator przedmiotu, np.BUTY_BUT; grafika ładowana po nazwiePARENT— indeks wiersza w<SCENA>.DTA, z którego przedmiot został podniesiony (-1= znikąd). Przy odkładaniu gra ustawia temu obiektowiIPARAM0 = 1BASE— lokacja pochodzenia (wartośćG_SARCADEOBJECTSw chwili podniesienia)EMPTY— mylące:1= slot zajęty,0= wolny (__LOAD_ITEMS__pokazuje przedmiot gdyEMPTY == 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 postacicommon/QUESTIONS<s>.ARR(w zapisie) — pytania, które Reksio aktualnie ma (ARRBASEQUESTIONS); zadanie pytania usuwa je stąd i dokłada do_HISTcommon/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
BFITMP61zapisuje przezG_ARRREXSPELLS^SAVE("SPELLS0.DTA")— zła nazwa i złe rozszerzenie, bez$COMMON\. Sam awans nie trafia doSPELLS0.ARRtą drogą; ratuje to dopiero najbliższyADDEXP→SAVEEXP.SAVEEXPsprawdzaGETSIZE() < 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, boNEWGAMEtworzy 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
.DTAmusi 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ę. ITEMSzawsze 10 wierszy — pętle są zahardkodowane na0..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 —
SAVEGAMEnadpisze 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.